forked from scalameta/metals
/
MtagsResolver.scala
312 lines (276 loc) · 9.85 KB
/
MtagsResolver.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
package scala.meta.internal.metals
import java.util.concurrent.ConcurrentHashMap
import scala.concurrent.duration._
import scala.util.control.NonFatal
import scala.meta.internal.jdk.CollectionConverters._
import scala.meta.internal.metals.BuildInfo
import scala.meta.internal.semver.SemVer
import coursierapi.error.SimpleResolutionError
import org.jsoup.Jsoup
trait MtagsResolver {
/**
* Try and resolve mtags module for a given version of Scala.
* Can contain a bunch of fallbacks in case of non stable versions.
* @return information to use and load the presentation compiler implementation
*/
def resolve(scalaVersion: String): Option[MtagsBinaries]
/**
* Check if a given Scala version is supported in Metals.
*
* @param version Scala version to check
*/
def isSupportedScalaVersion(version: String): Boolean =
resolve(version).isDefined
/**
* Check if this version of Scala is supported in a previous
* binary compatible Metals version. Needed for the doctor.
* @param version scala version to check
*/
def isSupportedInOlderVersion(version: String): Boolean
}
object MtagsResolver {
def default(): MtagsResolver = new Default
/**
* Map of removed Scala versions since 0.11.10.
* Points to the last Metals version that supported it.
*/
private val removedScalaVersions = Map(
"2.13.1" -> "0.11.10",
"2.13.2" -> "0.11.10",
"2.13.3" -> "0.11.12",
"2.13.4" -> "1.0.1",
"2.13.5" -> "1.2.2",
"2.12.9" -> "0.11.10",
"2.12.10" -> "0.11.12",
"2.12.11" -> "1.2.2",
"3.0.0" -> "0.11.10",
"3.0.1" -> "0.11.10",
"3.0.2" -> "0.11.12",
"3.2.2-RC1" -> "0.11.10",
"3.3.0-RC1" -> "0.11.10",
"3.3.0-RC2" -> "0.11.11",
"3.3.0-RC3" -> "0.11.12",
"3.3.0-RC4" -> "0.11.12",
"3.3.0-RC5" -> "0.11.12",
"3.3.0-RC6" -> "0.11.12",
"3.3.1-RC1" -> "0.11.12",
"3.3.1-RC2" -> "0.11.12",
"3.3.1-RC3" -> "0.11.12",
"3.3.1-RC4" -> "1.0.0",
"3.3.1-RC5" -> "1.0.0",
"3.3.1-RC6" -> "1.0.1",
"3.3.1-RC7" -> "1.0.1",
"3.3.0" -> "1.2.2",
"3.3.2-RC1" -> "1.2.2",
"3.3.2-RC2" -> "1.2.2",
"3.3.2-RC3" -> "1.2.2",
)
class Default extends MtagsResolver {
private val firstScala3PCVersion = BuildInfo.firstScala3PCVersion
private val states =
new ConcurrentHashMap[String, State]()
def hasStablePresentationCompiler(scalaVersion: String): Boolean =
SemVer.isCompatibleVersion(
firstScala3PCVersion,
scalaVersion,
)
def isSupportedInOlderVersion(version: String): Boolean =
removedScalaVersions.contains(version)
def resolve(scalaVersion: String): Option[MtagsBinaries] = {
resolve(scalaVersion, original = None)
}
private object ResolveType extends Enumeration {
val Regular, StablePC, Nightly = Value
}
/**
* Resolving order is following:
* 1. Built-in mtags matching scala version
* 2. Mtags matching exact scala version
* 3. Stable presentation compiler matching exact scala version
* 4. If scala version is a nightly, try to find latest supported snapshot
*/
private def resolve(
scalaVersion: String,
original: Option[String],
resolveType: ResolveType.Value = ResolveType.Regular,
): Option[MtagsBinaries] = {
def fetch(tries: Int = 0): State = logResolution {
try {
val metalsVersion = removedScalaVersions.getOrElse(
scalaVersion,
BuildInfo.metalsVersion,
)
if (metalsVersion != BuildInfo.metalsVersion) {
scribe.warn(
s"$scalaVersion is no longer supported in the current Metals versions, using the last known supported version $metalsVersion"
)
}
val jars = resolveType match {
case ResolveType.StablePC =>
Embedded.downloadScala3PresentationCompiler(scalaVersion)
case _ => Embedded.downloadMtags(scalaVersion, metalsVersion)
}
State.Success(
MtagsBinaries.Artifacts(
scalaVersion,
jars,
resolveType == ResolveType.StablePC,
)
)
} catch {
case NonFatal(e) =>
State.Failure(System.currentTimeMillis(), tries, e)
}
}
def shouldResolveAgain(failure: State.Failure): Boolean = {
failure.tries < State.maxTriesInARow ||
(System
.currentTimeMillis() - failure.lastTryMillis) > 5.minutes.toMillis
}
def logResolution(state: State): State = {
state match {
case _: State.Success =>
val msg = resolveType match {
case ResolveType.Regular => s"Resolved mtags for $scalaVersion"
case ResolveType.StablePC =>
s"Resolved Scala 3 presentation compiler for $scalaVersion"
case ResolveType.Nightly =>
s"Resolved latest nightly mtags version: $scalaVersion"
}
scribe.debug(msg)
case _: State.Failure
if !hasStablePresentationCompiler(scalaVersion) =>
val errorMsg = resolveType match {
case ResolveType.Regular =>
s"Failed to resolve mtags for $scalaVersion"
case ResolveType.StablePC =>
s"Failed to resolve Scala 3 presentation compiler for $scalaVersion"
case ResolveType.Nightly =>
s"Failed to resolve latest nightly mtags version: $scalaVersion"
}
scribe.info(errorMsg)
case _ =>
}
state
}
// The metals_2.12 artifact depends on mtags_2.12.x where "x" matches
// `mtags.BuildInfo.scalaCompilerVersion`. In the case when
// `info.getScalaVersion == mtags.BuildInfo.scalaCompilerVersion` then we
// skip fetching the mtags module from Maven.
if (MtagsBinaries.isBuildIn(scalaVersion)) {
Some(MtagsBinaries.BuildIn)
} else {
val computed = states.compute(
original.getOrElse(scalaVersion),
(_, value) => {
value match {
case null => fetch()
case succ: State.Success => succ
case failure: State.Failure =>
if (shouldResolveAgain(failure))
fetch(failure.tries + 1)
else {
failure
}
}
},
)
def logError(e: Throwable): Unit = {
val msg = s"Failed to fetch mtags for ${scalaVersion}"
e match {
case _: SimpleResolutionError =>
// no need to log traces for coursier error
// all explanation is in message
scribe.error(msg + "\n" + e.getMessage())
case _ =>
scribe.error(msg, e)
}
}
computed match {
case State.Success(v) =>
Some(v)
// Fallback to Stable PC version
case _: State.Failure
if resolveType != ResolveType.StablePC &&
hasStablePresentationCompiler(scalaVersion) =>
resolve(
scalaVersion,
None,
ResolveType.StablePC,
)
// Try to download latest supported snapshot
case _: State.Failure
if resolveType != ResolveType.Nightly &&
scalaVersion.contains("NIGHTLY") ||
scalaVersion.contains("nonbootstrapped") =>
findLatestSnapshot(scalaVersion) match {
case None => None
case Some(latestSnapshot) =>
scribe.warn(s"Using latest stable version $latestSnapshot")
resolve(
latestSnapshot,
Some(scalaVersion),
ResolveType.Nightly,
)
}
case failure: State.Failure =>
logError(failure.exception)
None
case _ => None
}
}
}
/**
* Nightlies version are able to work with artifacts compiled within the
* same RC version.
*
* For example 3.2.2-RC1-bin-20221009-2052fc2-NIGHTLY presentation compiler
* will work with classfiles compiled with 3.2.2-RC1-bin-20220910-ac6cd1c-NIGHTLY
*
* @param exactVersion version we failed to find and looking for an alternative for
* @return latest supported nightly version by thise version of metals
*/
private def findLatestSnapshot(exactVersion: String): Option[String] = try {
val metalsVersion = BuildInfo.metalsVersion
// strip timestamp to get only 3.2.2-RC1
val rcVersion = SemVer.Version
.fromString(exactVersion)
.copy(nightlyDate = None)
.toString()
val url =
s"https://oss.sonatype.org/content/repositories/snapshots/org/scalameta/"
val allScalametaArtifacts = Jsoup.connect(url).get
// find all the nightlies for current RC
val lastNightlies = allScalametaArtifacts
.select("a")
.asScala
.filter { a =>
val name = a.text()
name.contains("NIGHTLY") && name.contains(rcVersion)
}
// find last supported Scala version for this metals version
lastNightlies.reverseIterator
.find { nightlyLink =>
val link = nightlyLink.attr("href")
val mtagsPage = Jsoup.connect(link).get
mtagsPage
.select("a")
.asScala
.find(_.text() == metalsVersion + "/")
.isDefined
}
.map(_.text().stripPrefix("mtags_").stripSuffix("/"))
} catch {
case NonFatal(t) =>
scribe.error("Could not check latest nightlies", t)
None
}
sealed trait State
object State {
val maxTriesInARow: Int = 2
case class Success(v: MtagsBinaries.Artifacts) extends State
case class Failure(lastTryMillis: Long, tries: Int, exception: Throwable)
extends State
}
}
}