Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: Simplify resolving presentation compiler #6258

Merged
merged 2 commits into from
Mar 27, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 34 additions & 130 deletions metals/src/main/scala/scala/meta/internal/metals/MtagsResolver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,13 @@ 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]
Expand Down Expand Up @@ -93,27 +88,36 @@ object MtagsResolver {
removedScalaVersions.contains(version)

def resolve(scalaVersion: String): Option[MtagsBinaries] = {
resolve(scalaVersion, original = None)
if (hasStablePresentationCompiler(scalaVersion))
resolve(
scalaVersion,
original = None,
resolveType = ResolveType.StablePC,
)
else
resolve(
scalaVersion,
original = None,
resolveType = ResolveType.Regular,
)
}

private object ResolveType extends Enumeration {
val Regular, StablePC, Nightly = Value
val Regular, StablePC = 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
* 2. If presentation compiler is available we resolve it otherwise we resolve mtags.
*/
private def resolve(
scalaVersion: String,
original: Option[String],
resolveType: ResolveType.Value = ResolveType.Regular,
resolveType: ResolveType.Value,
): Option[MtagsBinaries] = {

def fetch(tries: Int = 0): State = logResolution {
def fetch(fetchResolveType: ResolveType.Value, tries: Int = 5): State =
try {
val metalsVersion = removedScalaVersions.getOrElse(
scalaVersion,
Expand All @@ -124,7 +128,7 @@ object MtagsResolver {
s"$scalaVersion is no longer supported in the current Metals versions, using the last known supported version $metalsVersion"
)
}
val jars = resolveType match {
val jars = fetchResolveType match {
case ResolveType.StablePC =>
Embedded.downloadScala3PresentationCompiler(scalaVersion)
case _ => Embedded.downloadMtags(scalaVersion, metalsVersion)
Expand All @@ -134,16 +138,17 @@ object MtagsResolver {
MtagsBinaries.Artifacts(
scalaVersion,
jars,
resolveType == ResolveType.StablePC,
fetchResolveType == ResolveType.StablePC,
)
)
} catch {
case NonFatal(_) if tries > 0 =>
fetch(fetchResolveType, tries - 1)
case NonFatal(e) =>
State.Failure(System.currentTimeMillis(), tries, e)
State.Failure(System.currentTimeMillis(), e)
}
}

def shouldResolveAgain(failure: State.Failure): Boolean = {
failure.tries < State.maxTriesInARow ||
(System
.currentTimeMillis() - failure.lastTryMillis) > 5.minutes.toMillis
}
Expand All @@ -155,21 +160,16 @@ object MtagsResolver {
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) =>
case fail: State.Failure =>
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)
scribe.error(errorMsg, fail.exception)
case _ =>
}
state
Expand All @@ -186,126 +186,30 @@ object MtagsResolver {
original.getOrElse(scalaVersion),
(_, value) => {
value match {
case null => fetch()
case null => logResolution(fetch(resolveType))
case succ: State.Success => succ
case failure: State.Failure if shouldResolveAgain(failure) =>
logResolution(fetch(resolveType))
case failure: State.Failure =>
if (shouldResolveAgain(failure))
fetch(failure.tries + 1)
else {
failure
}
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
case State.Success(v) => Some(v)
case _: State.Failure => 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
case class Failure(
lastTryMillis: Long,
exception: Throwable,
) extends State
}
}

Expand Down
Loading