Skip to content
Draft
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object BazelMbtBuildSupport {
runTargets: Set[String],
classDirectoriesByTarget: Map[String, String],
dependencyModules: Seq[MbtDependencyModule],
scalaVersion: Option[String],
scalaVersionByNamespace: Map[String, Option[String]],
genSrcOutputsByTarget: Map[String, List[String]] = Map.empty,
): MbtBuild = {
val depModules = new ju.ArrayList[MbtDependencyModule]()
Expand All @@ -49,7 +49,11 @@ object BazelMbtBuildSupport {
if (granularity == BazelMbtNamespaceMode.Workspace) {
MbtBuild(
depModules,
singleNamespace(workspaceNamespaceName, Set.empty, scalaVersion),
singleNamespace(
workspaceNamespaceName,
Set.empty,
scalaVersionByNamespace.getOrElse(workspaceNamespaceName, None),
),
uncheckedSources = ju.Collections.emptyList(),
)
} else {
Expand Down Expand Up @@ -148,7 +152,7 @@ object BazelMbtBuildSupport {
externalDepsByNs.getOrElse(namespace, Set.empty),
runTargetsByNs.getOrElse(namespace, Set.empty),
classDirectoriesByNs.get(namespace),
scalaVersion,
scalaVersionByNamespace.getOrElse(namespace, None),
genSrcOutputsByNamespaces
.getOrElse(namespace, mutable.Buffer.empty)
.toSeq,
Expand All @@ -169,7 +173,7 @@ object BazelMbtBuildSupport {
allExtDeps,
runTargetsByNs.getOrElse(workspaceNamespaceName, Set.empty),
classDirectoriesByNs.get(workspaceNamespaceName),
scalaVersion,
scalaVersionByNamespace.getOrElse(workspaceNamespaceName, None),
allGenSrcOutputs,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import scala.meta.internal.metals.Tables
import scala.meta.internal.metals.UserConfiguration
import scala.meta.internal.metals.clients.language.MetalsLanguageClient
import scala.meta.internal.metals.mbt.MbtBuild
import scala.meta.internal.metals.mbt.MbtDependencyModule
import scala.meta.internal.process.ExitCodes
import scala.meta.io.AbsolutePath

Expand Down Expand Up @@ -108,11 +109,13 @@ abstract class BazelMbtImporter(
dependencyModules,
mavenHubs,
)
scalaVersionFromDeps <- queryScalaVersionFromDeps()
effectiveScalaVersion <- scalaVersionFromDeps match {
case Some(value) => Future.successful(Some(value))
case None => queryScalaVersion(targets)
}
scalaVersionByNamespace <- resolveScalaVersionByNamespace(
namespaceMode,
targets,
targetsXmlDump,
externalDeps,
outputBase,
)
build = BazelMbtBuildSupport.fromDiscovery(
namespaceMode,
targets,
Expand All @@ -124,7 +127,7 @@ abstract class BazelMbtImporter(
runTargets,
classDirectories,
dependencyModules,
effectiveScalaVersion,
scalaVersionByNamespace,
genSrcOutputsByTarget,
)
_ <- Future(Files.writeString(out.toNIO, MbtBuild.toJson(build)))
Expand Down Expand Up @@ -210,15 +213,94 @@ abstract class BazelMbtImporter(
}.toMap
}

private def queryScalaVersionFromDeps(): Future[Option[String]] = for {
queryOutput <- BazelQuery.allScalaLibrariesQuery.run(queryEnv)
lines = asLines(queryOutput)
} yield lines.flatMap(extractScalaVersionFromLabel).headOption
private def bazelLabelFromModuleId(
moduleId: String,
repositoryName: String,
): Option[String] = {
val parts = moduleId.split(":")
if (parts.length >= 2) {
val groupId = parts(0)
val artifactId = parts(1)
val sanitizedGroup = groupId.replace('.', '_').replace('-', '_')
val sanitizedArtifact = artifactId.replace('.', '_').replace('-', '_')
Some(s"@$repositoryName//:${sanitizedGroup}_$sanitizedArtifact")
} else None
}

private def normalizeBazelLabel(label: String): String = {
val withoutDoubleAt =
if (label.startsWith("@@")) label.substring(1) else label
withoutDoubleAt.replaceAll("~[^/]+", "")
}

private def resolveScalaVersionByNamespace(
namespaceMode: BazelMbtNamespaceMode,
targets: List[String],
targetsXmlDump: BazelTargetsXmlDump,
externalDeps: Map[String, List[String]],
outputBase: Option[Path],
): Future[Map[String, Option[String]]] = {
val scalaVersionAttributes = targetsXmlDump.getStrings("scala_version")
val namespaceToTargets = targets.groupBy(
BazelMbtBuildSupport.namespaceKey(namespaceMode, _)
)
val versionsByNamespace =
namespaceToTargets.map { case (ns, nsTargets) =>
val explicitVersions =
nsTargets.flatMap(t => scalaVersionAttributes.getOrElse(t, Nil))
val version = pickMaxScalaVersion(
if (explicitVersions.nonEmpty) explicitVersions
else
nsTargets
.flatMap(t => externalDeps.getOrElse(t, Nil))
.flatMap(extractScalaVersionFromLabel)
)
ns -> version
}
val namespacesNeedingFallback =
versionsByNamespace.collect { case (ns, None) => ns }
val globalFallback =
if (namespacesNeedingFallback.isEmpty) Future.successful(None)
else
parseScalaVersionFromRulesScalaConfig(outputBase)
.orElse(parseScalaVersionFromBuildFiles())
.map(v => Future.successful(Some(v)))
.getOrElse(queryScalaVersionFromDeps())

globalFallback.map { fallbackVersion =>
versionsByNamespace.map { case (ns, v) =>
ns -> v.orElse(fallbackVersion)
}
}
}

private def parseScalaVersionFromRulesScalaConfig(
outputBase: Option[Path]
): Option[String] = {
val versionPattern = """SCALA_VERSION\s*=\s*["'](\d+\.\d+\.\d+)["']""".r
outputBase.flatMap { base =>
val externalDir = base.resolve("external").toFile
val entries = externalDir.listFiles()
val configDir = Option(entries).flatMap(
_.find(f => f.isDirectory && f.getName.contains("rules_scala_config"))
.map(_.toPath)
)
configDir.flatMap { dir =>
val configFile = dir.resolve("config.bzl")
if (Files.exists(configFile)) {
val content = new String(Files.readAllBytes(configFile))
versionPattern.findFirstMatchIn(content).map(_.group(1))
} else None
}
}
}

private def queryScalaVersion(
@annotation.nowarn("msg=never used") targets: List[String]
): Future[Option[String]] =
Future.successful(parseScalaVersionFromBuildFiles())
private def queryScalaVersionFromDeps(): Future[Option[String]] =
BazelQuery.allScalaLibrariesQuery.run(queryEnv).map { queryOutput =>
pickMaxScalaVersion(
asLines(queryOutput).flatMap(extractScalaVersionFromLabel)
)
}

private def parseScalaVersionFromBuildFiles(): Option[String] = {
val versionPattern = """scala_version\s*=\s*["'](\d+\.\d+\.\d+)["']""".r
Expand All @@ -234,8 +316,19 @@ abstract class BazelMbtImporter(
extractFromFile(moduleFile).orElse(extractFromFile(workspaceFile))
}

private def pickMaxScalaVersion(versions: List[String]): Option[String] =
versions.maxByOption { v =>
val p = v.split('.')
(
p.lift(0).flatMap(_.toIntOption).getOrElse(0),
p.lift(1).flatMap(_.toIntOption).getOrElse(0),
p.lift(2).flatMap(_.toIntOption).getOrElse(0),
)
}

private def extractScalaVersionFromLabel(label: String): Option[String] = {
val versionPattern = """scala[_-]library[_-](\d+\.\d+\.\d+)""".r
val versionPattern =
"""scala(?:3-library_3|[_-]library)[_-](\d+\.\d+\.\d+)""".r
versionPattern.findFirstMatchIn(label).map(_.group(1))
}

Expand Down
94 changes: 93 additions & 1 deletion tests/slow/src/test/scala/tests/bazel/BazelMbtLspSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,56 @@ class BazelMbtLspSuite
private val catsVersion = "2.13.0"
private val jsoupVersion = "1.21.1"

private def mixedScalaVersionsLayout: String =
s"""|/.bazelproject
|targets:
| //...
|
|/MODULE.bazel
|bazel_dep(name = "rules_scala", version = "7.2.4")
|
|scala_config = use_extension("@rules_scala//scala/extensions:config.bzl", "scala_config")
|scala_config.settings(
| scala_version = "${V.scala213}",
| scala_versions = ["${V.scala213}", "${V.scala3}"],
|)
|
|scala_deps = use_extension("@rules_scala//scala/extensions:deps.bzl", "scala_deps")
|scala_deps.settings(fetch_sources = True)
|scala_deps.scala()
|scala_deps.scala(scala_version = "${V.scala3}")
|use_repo(scala_deps, "rules_scala_toolchains")
|
|register_toolchains("@rules_scala_toolchains//...:all")
|
|/lib2/BUILD
|load("@rules_scala//scala:scala.bzl", "scala_library")
|
|scala_library(
| name = "lib2",
| srcs = ["Lib2.scala"],
|)
|
|/lib2/Lib2.scala
|package lib2
|
|class Lib2
|
|/lib3/BUILD
|load("@rules_scala//scala:scala.bzl", "scala_library")
|
|scala_library(
| name = "lib3",
| srcs = ["Lib3.scala"],
| scala_version = "${V.scala3}",
|)
|
|/lib3/Lib3.scala
|package lib3
|
|class Lib3
|""".stripMargin

/** Same targets as [[BazelLspSuite]], plus a project view so MBT import scopes `bazel query`. */
private def bazelWorkspaceLayout: String = {
val projectView =
Expand Down Expand Up @@ -434,7 +484,7 @@ class BazelMbtLspSuite
| "org.typelevel:cats-core_2.13:2.13.0",
| "org.typelevel:cats-kernel_2.13:2.13.0"
| ],
| "scalaVersion": "2.13.14",
| "scalaVersion": "2.13.16",
| "dependsOn": [],
| "classDirectories": []
| }
Expand Down Expand Up @@ -866,4 +916,46 @@ class BazelMbtLspSuite
} yield ()
}

test("bazel-import-mbt-mixed-scala-versions") {
cleanWorkspace()
for {
_ <- initialize(
BazelBuildLayout(mixedScalaVersionsLayout, V.scala213, bazelVersion)
)
_ <- server.headServer.connectionProvider.buildServerPromise.future
mbtFile = workspace.resolve(".metals/mbt.json").readText
_ = assertNoDiff(
escapeMbtFile(mbtFile),
s"""|{
| "dependencyModules": [],
| "namespaces": {
| "//lib2": {
| "sources": [
| "lib2/Lib2.scala"
| ],
| "scalacOptions": [],
| "javacOptions": [],
| "dependencyModules": [],
| "scalaVersion": "${V.scala213}",
| "dependsOn": [],
| "classDirectories": []
| },
| "//lib3": {
| "sources": [
| "lib3/Lib3.scala"
| ],
| "scalacOptions": [],
| "javacOptions": [],
| "dependencyModules": [],
| "scalaVersion": "${V.scala3}",
| "dependsOn": [],
| "classDirectories": []
| }
| },
| "uncheckedSources": []
|}""".stripMargin,
)
} yield ()
}

}
Loading