-
Notifications
You must be signed in to change notification settings - Fork 202
/
BloopComponentCompiler.scala
381 lines (342 loc) · 14.8 KB
/
BloopComponentCompiler.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
package inc
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import _root_.bloop.io.AbsolutePath
import _root_.bloop.logging.DebugFilter
import _root_.bloop.logging.{Logger => BloopLogger}
import _root_.bloop.{DependencyResolution => BloopDependencyResolution}
import _root_.bloop.{ScalaInstance => BloopScalaInstance}
import sbt.internal.inc.classpath.ClasspathUtil
import sbt.io.IO
import sbt.librarymanagement._
import xsbti.ArtifactInfo._
import xsbti.ComponentProvider
import xsbti.Logger
import xsbti.compile.ClasspathOptionsUtil
import xsbti.compile.CompilerBridgeProvider
object BloopComponentCompiler {
import xsbti.compile.ScalaInstance
final val binSeparator = "-bin_"
final val javaClassVersion: String = System.getProperty("java.class.version")
final lazy val latestVersion: String = BloopComponentManager.version
def getComponentProvider(componentsDir: AbsolutePath): ComponentProvider = {
val componentsPath = componentsDir.underlying
if (!Files.exists(componentsPath)) Files.createDirectory(componentsPath)
getDefaultComponentProvider(componentsPath.toFile())
}
private val CompileConf = Some(Configurations.Compile.name)
def getModuleForBridgeSources(scalaInstance: ScalaInstance): ModuleID = {
def compilerBridgeId(scalaVersion: String) = {
// Defaults to bridge for 2.13 for Scala versions bigger than 2.13.x
scalaVersion match {
case sc if (sc startsWith "0.") => "dotty-sbt-bridge"
case sc if (sc startsWith "3.") => "scala3-sbt-bridge"
case sc if (sc startsWith "2.10.") => "compiler-bridge_2.10"
case sc if (sc startsWith "2.11.") => "compiler-bridge_2.11"
case sc if (sc startsWith "2.12.") => "compiler-bridge_2.12"
case _ => "compiler-bridge_2.13"
}
}
val (isDotty, organization, version) = scalaInstance match {
case instance: BloopScalaInstance if instance.isDotty =>
(true, instance.organization, instance.version)
case _ => (false, "org.scala-sbt", latestVersion)
}
val bridgeId = compilerBridgeId(scalaInstance.version)
val module = ModuleID(organization, bridgeId, version).withConfigurations(CompileConf)
if (isDotty) module else module.sources()
}
/**
* Returns the id for the compiler interface component.
*
* The ID contains the following parts:
* - The organization, name and revision.
* - The bin separator to make clear the jar represents binaries.
* - The Scala version for which the compiler interface is meant to.
* - The JVM class version.
*
* Example: "org.scala-sbt-compiler-bridge-1.0.0-bin_2.11.7__50.0".
*
* @param sources The moduleID representing the compiler bridge sources.
* @param scalaInstance The scala instance that sets the scala version for the id.
* @return The complete jar identifier for the bridge sources.
*/
def getBridgeComponentId(sources: ModuleID, scalaInstance: ScalaInstance): String = {
val id = s"${sources.organization}-${sources.name}-${sources.revision}"
val scalaVersion = scalaInstance.actualVersion()
s"$id$binSeparator${scalaVersion}__$javaClassVersion"
}
/** Defines the internal implementation of a bridge provider. */
private class BloopCompilerBridgeProvider(
userProvidedBridgeSources: Option[ModuleID],
manager: BloopComponentManager,
logger: BloopLogger
) extends CompilerBridgeProvider {
/**
* Defines a richer interface for Scala users that want to pass in an explicit module id.
*
* Note that this method cannot be defined in [[CompilerBridgeProvider]] because [[ModuleID]]
* is a Scala-defined class to which the compiler bridge cannot depend on.
*/
private def compiledBridge(bridgeSources: ModuleID, scalaInstance: ScalaInstance): File = {
val raw = new RawCompiler(scalaInstance, ClasspathOptionsUtil.auto, logger)
val zinc = new BloopComponentCompiler(raw, manager, bridgeSources, logger)
logger.debug(s"Getting $bridgeSources for Scala ${scalaInstance.version}")(
DebugFilter.Compilation
)
zinc.compiledBridgeJar
}
override def fetchCompiledBridge(scalaInstance: ScalaInstance, logger: Logger): File = {
val bridgeSources =
userProvidedBridgeSources.getOrElse(getModuleForBridgeSources(scalaInstance))
compiledBridge(bridgeSources, scalaInstance)
}
private case class ScalaArtifacts(compiler: File, library: File, others: Vector[File])
private def getScalaArtifacts(scalaVersion: String, logger: BloopLogger): ScalaArtifacts = {
def isPrefixedWith(artifact: File, prefix: String) = artifact.getName.startsWith(prefix)
val allArtifacts = {
BloopDependencyResolution
.resolve(
List(
BloopDependencyResolution.Artifact(ScalaOrganization, ScalaCompilerID, scalaVersion)
),
logger
)
.map(_.toFile)
.toVector
}
logger.debug(s"Resolved scala artifacts for $scalaVersion: $allArtifacts")(
DebugFilter.Compilation
)
val isScalaCompiler = (f: File) => isPrefixedWith(f, "scala-compiler-")
val isScalaLibrary = (f: File) => isPrefixedWith(f, "scala-library-")
val maybeScalaCompiler = allArtifacts.find(isScalaCompiler)
val maybeScalaLibrary = allArtifacts.find(isScalaLibrary)
val others = allArtifacts.filterNot(a => isScalaCompiler(a) || isScalaLibrary(a))
val scalaCompilerJar = maybeScalaCompiler.getOrElse(throw MissingScalaJar.compiler)
val scalaLibraryJar = maybeScalaLibrary.getOrElse(throw MissingScalaJar.library)
ScalaArtifacts(scalaCompilerJar, scalaLibraryJar, others)
}
override def fetchScalaInstance(scalaVersion: String, unusedLogger: Logger): ScalaInstance = {
val scalaArtifacts = getScalaArtifacts(scalaVersion, logger)
val scalaCompiler = scalaArtifacts.compiler
val scalaLibrary = scalaArtifacts.library
val jarsToLoad = (scalaCompiler +: scalaLibrary +: scalaArtifacts.others).toArray
assert(jarsToLoad.forall(_.exists), "One or more jar(s) in the Scala instance do not exist.")
val loaderLibraryOnly = ClasspathUtil.toLoader(Vector(scalaLibrary.toPath()))
val jarsToLoad2 = jarsToLoad.toVector.filterNot(_ == scalaLibrary)
val loader = ClasspathUtil.toLoader(jarsToLoad2.map(_.toPath()), loaderLibraryOnly)
val properties = ResourceLoader.getSafePropertiesFor("compiler.properties", loader)
val loaderVersion = Option(properties.getProperty("version.number"))
val scalaV = loaderVersion.getOrElse("unknown")
new inc.ScalaInstance(
scalaV,
loader,
loader,
loaderLibraryOnly,
Array(scalaLibrary),
jarsToLoad,
jarsToLoad,
loaderVersion
)
}
}
def interfaceProvider(
compilerBridgeSource: ModuleID,
manager: BloopComponentManager,
logger: BloopLogger
): CompilerBridgeProvider = {
val bridgeSources = Some(compilerBridgeSource)
new BloopCompilerBridgeProvider(
bridgeSources,
manager,
logger
)
}
/** Defines a default component provider that manages the component in a given directory. */
final class DefaultComponentProvider(targetDir: File) extends ComponentProvider {
import sbt.io.syntax._
private val LockFile = targetDir / "lock"
override def lockFile(): File = LockFile
override def componentLocation(id: String): File = targetDir / id
override def component(componentID: String): Array[File] =
IO.listFiles(targetDir / componentID)
override def defineComponent(componentID: String, files: Array[File]): Unit =
files.foreach(f => IO.copyFile(f, targetDir / componentID / f.getName))
override def addToComponent(componentID: String, files: Array[File]): Boolean = {
defineComponent(componentID, files)
true
}
}
def getDefaultComponentProvider(targetDir: File): ComponentProvider = {
require(targetDir.isDirectory)
new DefaultComponentProvider(targetDir)
}
}
/**
* Component compiler which is able to to retrieve the compiler bridge sources
* `sourceModule` using a `DependencyResolution` instance.
* The compiled classes are cached using the provided component manager according
* to the actualVersion field of the RawCompiler.
*/
private[inc] class BloopComponentCompiler(
compiler: RawCompiler,
manager: BloopComponentManager,
bridgeSources: ModuleID,
logger: BloopLogger
) {
implicit val debugFilter: DebugFilter.Compilation.type = DebugFilter.Compilation
def compiledBridgeJar: File = {
val jarBinaryName = {
val instance = compiler.scalaInstance
val bridgeName = {
if (!HydraSupport.isEnabled(instance)) bridgeSources
else HydraSupport.getModuleForBridgeSources(instance)
}
BloopComponentCompiler.getBridgeComponentId(bridgeName, compiler.scalaInstance)
}
manager.file(jarBinaryName)(IfMissing.define(true, compileAndInstall(jarBinaryName)))
}
/**
* Resolves the compiler bridge sources, compiles them and installs the sbt component
* in the local filesystem to make sure that it's reused the next time is required.
*
* @param compilerBridgeId The identifier for the compiler bridge sources.
*/
private def compileAndInstall(compilerBridgeId: String): Unit = {
IO.withTemporaryDirectory { binaryDirectory =>
val target = new File(binaryDirectory, s"$compilerBridgeId.jar")
IO.withTemporaryDirectory { _ =>
val shouldResolveSources =
bridgeSources.explicitArtifacts.exists(_.`type` == "src")
val allArtifacts = BloopDependencyResolution.resolveWithErrors(
List(
BloopDependencyResolution
.Artifact(bridgeSources.organization, bridgeSources.name, bridgeSources.revision)
),
logger,
resolveSources = shouldResolveSources
) match {
case Right(paths) => paths.map(_.underlying).toVector
case Left(t) =>
val msg = s"Couldn't retrieve module $bridgeSources"
throw new InvalidComponent(msg, t)
}
if (!shouldResolveSources) {
// This is usually true in the Dotty case, that has a pre-compiled compiler
manager.define(compilerBridgeId, allArtifacts.map(_.toFile()))
} else {
val (sources, xsbtiJars) =
allArtifacts.partition(_.toFile.getName.endsWith("-sources.jar"))
val (toCompileID, allSources) = {
if (!HydraSupport.isEnabled(compiler.scalaInstance)) (bridgeSources.name, sources)
else {
val hydraBridgeModule = HydraSupport.getModuleForBridgeSources(compiler.scalaInstance)
mergeBloopAndHydraBridges(sources, hydraBridgeModule) match {
case Right(mergedHydraBridgeSourceJar) =>
(hydraBridgeModule.name, mergedHydraBridgeSourceJar)
case Left(error) =>
logger.error(
s"Unexpected error when merging Bloop and Hydra bridges. Reason: $error"
)
logger.trace(error)
// Throw, there's no reasonable fallback method if Hydra sources fail to be merged
throw error
}
}
}
AnalyzingCompiler.compileSources(
allSources,
target.toPath(),
xsbtiJars,
toCompileID,
compiler,
logger
)
manager.define(compilerBridgeId, Seq(target))
}
}
}
}
private def mergeBloopAndHydraBridges(
bloopBridgeSourceJars: Vector[Path],
hydraBridgeModule: ModuleID
): Either[InvalidComponent, Vector[Path]] = {
val hydraSourcesJars = BloopDependencyResolution.resolveWithErrors(
List(
BloopDependencyResolution
.Artifact(
hydraBridgeModule.organization,
hydraBridgeModule.name,
hydraBridgeModule.revision
)
),
logger,
resolveSources = true,
additionalRepositories = List(HydraSupport.resolver)
) match {
case Right(paths) => Right(paths.map(_.underlying).toVector)
case Left(t) =>
val msg = s"Couldn't retrieve module $hydraBridgeModule"
Left(new InvalidComponent(msg, t))
}
import sbt.io.IO.{zip, unzip, withTemporaryDirectory, relativize}
hydraSourcesJars match {
case Right(sourceJar +: otherJars) =>
if (otherJars.nonEmpty) {
logger.warn(
s"Expected to retrieve a single bridge jar for Hydra. Keeping $sourceJar and ignoring $otherJars"
)
}
bloopBridgeSourceJars.headOption match {
case None =>
logger.debug("Missing stock compiler bridge, proceeding with all Hydra bridge sources.")
case Some(stockCompilerBridge) =>
logger.debug(s"Merging ${stockCompilerBridge} with $sourceJar")
}
withTemporaryDirectory { tempDir =>
val hydraSourceContents = unzip(sourceJar.toFile, tempDir)
logger.debug(s"Sources from hydra bridge: $hydraSourceContents")
// Unfortunately we can only use names to filter out, let's hope there's no clashes
val filterOutConflicts = new sbt.io.NameFilter {
val hydraNameDenylist = hydraSourceContents.map(_.getName)
def accept(rawPath: String): Boolean = {
val path = Paths.get(rawPath)
val fileName = path.getFileName.toString
!hydraNameDenylist.contains(fileName)
}
}
// Extract bridge source contens in same folder with Hydra contents having preference
val regularSourceContents = bloopBridgeSourceJars.foldLeft(Set.empty[File]) {
case (extracted, sourceJar) =>
extracted ++ unzip(sourceJar.toFile(), tempDir, filter = filterOutConflicts)
}
logger.debug(s"Sources from bloop bridge: $regularSourceContents")
val mergedJar = Files.createTempFile(HydraSupport.bridgeNamePrefix, "merged")
logger.debug(s"Merged jar destination: $mergedJar")
val allSourceContents =
(hydraSourceContents ++ regularSourceContents).map(s => s -> relativize(tempDir, s).get)
zip(allSourceContents.toSeq, mergedJar.toFile(), time = None)
Right(Vector(mergedJar))
}
case Right(Seq()) =>
val msg =
s"""Hydra resolution failure: bridge source jar is empty!
| -> Hydra coordinates ($hydraBridgeModule)
| -> Report this error to support@triplequote.com.
""".stripMargin
Left(new InvalidComponent(msg))
case error @ Left(_) => error
}
}
}