Skip to content

Commit

Permalink
improvement: auto detect project root (#5576)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasiaMarek authored Sep 22, 2023
1 parent f448ae0 commit 4653648
Show file tree
Hide file tree
Showing 31 changed files with 477 additions and 193 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package scala.meta.internal.bsp

import java.nio.file.Files
import java.nio.file.StandardCopyOption

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.control.NonFatal

import scala.meta.internal.bsp.BspConfigGenerationStatus._
import scala.meta.internal.builds.BuildServerProvider
Expand Down Expand Up @@ -31,10 +35,33 @@ final class BspConfigGenerator(
.run(
s"${buildTool.getBuildServerName} bspConfig",
args,
workspace,
buildTool.projectRoot,
buildTool.redirectErrorOutput,
)
.map(BspConfigGenerationStatus.fromExitCode)
.map {
case Generated if buildTool.projectRoot != workspace =>
try {
val bsp = ".bsp"
workspace.resolve(bsp).createDirectories()
val buildToolBspDir = buildTool.projectRoot.resolve(bsp).toNIO
val workspaceBspDir = workspace.resolve(bsp).toNIO
buildToolBspDir.toFile.listFiles().foreach { file =>
val path = file.toPath()
if (!file.isDirectory() && path.filename.endsWith(".json")) {
val to =
workspaceBspDir.resolve(path.relativize(buildToolBspDir))
Files.move(path, to, StandardCopyOption.REPLACE_EXISTING)
}
}
Files.delete(buildToolBspDir)
Generated
} catch {
case NonFatal(_) =>
Failed(Right("Could not move bsp config from project root"))
}
case status => status
}

/**
* Given multiple build tools that are all BuildServerProviders, allow the
Expand Down
39 changes: 25 additions & 14 deletions metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ class BspConnector(
* of the bsp entry has already happened at this point.
*/
def connect(
projectRoot: AbsolutePath,
workspace: AbsolutePath,
userConfiguration: UserConfiguration,
shellRunner: ShellRunner,
)(implicit ec: ExecutionContext): Future[Option[BspSession]] = {
def connect(
workspace: AbsolutePath,
projectRoot: AbsolutePath,
bspTraceRoot: AbsolutePath,
addLivenessMonitor: Boolean,
): Future[Option[BuildServerConnection]] = {
scribe.info("Attempting to connect to the build server...")
Expand All @@ -87,18 +89,24 @@ class BspConnector(
Future.successful(None)
case ResolvedBloop =>
bloopServers
.newServer(workspace, userConfiguration, addLivenessMonitor)
.newServer(
projectRoot,
bspTraceRoot,
userConfiguration,
addLivenessMonitor,
)
.map(Some(_))
case ResolvedBspOne(details)
if details.getName() == SbtBuildTool.name =>
tables.buildServers.chooseServer(SbtBuildTool.name)
val shouldReload = SbtBuildTool.writeSbtMetalsPlugins(workspace)
val shouldReload = SbtBuildTool.writeSbtMetalsPlugins(projectRoot)
val connectionF =
for {
_ <- SbtBuildTool(workspace, () => userConfiguration)
.ensureCorrectJavaVersion(shellRunner, workspace, client)
_ <- SbtBuildTool(projectRoot, () => userConfiguration)
.ensureCorrectJavaVersion(shellRunner, projectRoot, client)
connection <- bspServers.newServer(
workspace,
projectRoot,
bspTraceRoot,
details,
addLivenessMonitor,
)
Expand All @@ -112,7 +120,7 @@ class BspConnector(
case ResolvedBspOne(details) =>
tables.buildServers.chooseServer(details.getName())
bspServers
.newServer(workspace, details, addLivenessMonitor)
.newServer(projectRoot, bspTraceRoot, details, addLivenessMonitor)
.map(Some(_))
case ResolvedMultiple(_, availableServers) =>
val distinctServers = availableServers
Expand Down Expand Up @@ -146,15 +154,16 @@ class BspConnector(
)
_ = tables.buildServers.chooseServer(item.getName())
conn <- bspServers.newServer(
workspace,
projectRoot,
bspTraceRoot,
item,
addLivenessMonitor,
)
} yield Some(conn)
}
}

connect(workspace, addLivenessMonitor = true).flatMap {
connect(projectRoot, workspace, addLivenessMonitor = true).flatMap {
possibleBuildServerConn =>
possibleBuildServerConn match {
case None => Future.successful(None)
Expand All @@ -163,8 +172,8 @@ class BspConnector(
// NOTE: (ckipp01) we special case this here since sbt bsp server
// doesn't yet support metabuilds. So in the future when that
// changes, re-work this and move the creation of this out above
val metaConns = sbtMetaWorkspaces(workspace).map(
connect(_, addLivenessMonitor = false)
val metaConns = sbtMetaWorkspaces(workspace).map(root =>
connect(root, root, addLivenessMonitor = false)
)
Future
.sequence(metaConns)
Expand Down Expand Up @@ -220,8 +229,8 @@ class BspConnector(
* Runs "Switch build server" command, returns true if build server choice
* was changed.
*
* NOTE: that in most cases this doesn't actaully change your build server
* and connect to it, but stores that you want to chage it unless you are
* NOTE: that in most cases this doesn't actually change your build server
* and connect to it, but stores that you want to change it unless you are
* choosing Bloop, since in that case it's special cased and does start it.
*/
def switchBuildServer(
Expand Down Expand Up @@ -255,7 +264,9 @@ class BspConnector(
BuildServerProvider,
BspConnectionDetails,
]] = {
if (bloopPresent || buildTools.loadSupported().nonEmpty)
if (
bloopPresent || buildTools.loadSupported().exists(_.isBloopDefaultBsp)
)
new BspConnectionDetails(
BloopServers.name,
ImmutableList.of(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ final class BspServers(

def newServer(
projectDirectory: AbsolutePath,
bspTraceRoot: AbsolutePath,
details: BspConnectionDetails,
addLivenessMonitor: Boolean,
): Future[BuildServerConnection] = {
Expand Down Expand Up @@ -135,6 +136,7 @@ final class BspServers(

BuildServerConnection.fromSockets(
projectDirectory,
bspTraceRoot,
buildClient,
client,
newConnection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ object ScalaCliBspScope {
)
}

private def scalaCliBspRoot(root: AbsolutePath): List[AbsolutePath] =
def scalaCliBspRoot(root: AbsolutePath): List[AbsolutePath] =
for {
path <- ScalaCliBuildTool.pathsToScalaCliBsp(root)
text <- path.readTextOpt.toList
Expand All @@ -25,7 +25,7 @@ object ScalaCliBspScope {
case "bsp" :: tail => dropOptions(tail).takeWhile(!_.startsWith("-"))
case _ => Nil
}
rootPath <- Try(AbsolutePath(rootArg).dealias).toOption
rootPath <- Try(AbsolutePath(rootArg)(root).dealias).toOption
if rootPath.exists
} yield rootPath

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ final class BloopInstall(
.run(
s"${buildTool.executableName} bloopInstall",
args,
workspace,
buildTool.projectRoot,
buildTool.redirectErrorOutput,
Map(
"COURSIER_PROGRESS" -> "disable",
Expand Down Expand Up @@ -165,7 +165,7 @@ final class BloopInstall(
)(implicit ec: ExecutionContext): Future[Confirmation] = {
tables.digests.setStatus(digest, Status.Requested)
val (params, yes) =
if (buildTools.isBloop) {
if (buildTools.isBloop(buildTool.projectRoot)) {
ImportBuildChanges.params(buildTool.toString) ->
ImportBuildChanges.yes
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ trait BuildTool {

def isBloopDefaultBsp = true

def projectRoot: AbsolutePath

}

object BuildTool {
Expand Down
114 changes: 81 additions & 33 deletions metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.nio.file.Files
import java.util.Properties
import java.util.concurrent.atomic.AtomicReference

import scala.meta.internal.bsp.ScalaCliBspScope
import scala.meta.internal.io.PathIO
import scala.meta.internal.metals.BloopServers
import scala.meta.internal.metals.MetalsEnrichments._
Expand All @@ -30,31 +31,38 @@ final class BuildTools(
) {
private val lastDetectedBuildTools = new AtomicReference(Set.empty[String])
// NOTE: We do a couple extra check here before we say a workspace with a
// `.bsp` is auto-connectable, and we ensure that a user has explicity chosen
// `.bsp` is auto-connectable, and we ensure that a user has explicitly chosen
// to use another build server besides Bloop or it's a BSP server for a build
// tool we don't support. If this isn't done, it causes unexpected warnings
// since if a `.bsp/<something>.json` exists before a `.bloop` one does in a
// workspace with a build tool we support, we will attempt to autoconnect to
// Bloop since Metals thinks it's in state that's auto-connectable before the
// user is even prompted.
def isAutoConnectable: Boolean = {
isBloop || (isBsp && all.isEmpty) || (isBsp && explicitChoiceMade()) || (isBsp && isBazel)
}
def isBloop: Boolean = {
hasJsonFile(workspace.resolve(".bloop"))
def isAutoConnectable(
maybeProjectRoot: Option[AbsolutePath] = None
): Boolean = {
maybeProjectRoot
.map(isBloop)
.getOrElse(
isBloop
) || (isBsp && all.isEmpty) || (isBsp && explicitChoiceMade()) || (isBsp && isBazel)
}
def isBloop(root: AbsolutePath): Boolean = hasJsonFile(root.resolve(".bloop"))
def bloopProject: Option[AbsolutePath] = searchForBuildTool(isBloop)
def isBloop: Boolean = bloopProject.isDefined
def isBsp: Boolean = {
hasJsonFile(workspace.resolve(".bsp")) ||
bspGlobalDirectories.exists(hasJsonFile)
}
private def hasJsonFile(dir: AbsolutePath): Boolean = {
dir.list.exists(_.extension == "json")
}

// Returns true if there's a build.sbt file or project/build.properties with sbt.version
def isSbt: Boolean = {
workspace.resolve("build.sbt").isFile || {
def sbtProject: Option[AbsolutePath] = searchForBuildTool { root =>
root.resolve("build.sbt").isFile || {
val buildProperties =
workspace.resolve("project").resolve("build.properties")
root.resolve("project").resolve("build.properties")
buildProperties.isFile && {
val props = new Properties()
val in = Files.newInputStream(buildProperties.toNIO)
Expand All @@ -64,31 +72,72 @@ final class BuildTools(
}
}
}
def isMill: Boolean = workspace.resolve("build.sc").isFile
def isScalaCli: Boolean =
ScalaCliBuildTool
.pathsToScalaCliBsp(workspace)
.exists(_.isFile) || workspace.resolve("project.scala").isFile
def isGradle: Boolean = {
def isSbt: Boolean = sbtProject.isDefined
def millProject: Option[AbsolutePath] = searchForBuildTool(
_.resolve("build.sc").isFile
)
def isMill: Boolean = millProject.isDefined
def scalaCliProject: Option[AbsolutePath] =
searchForBuildTool(_.resolve("project.scala").isFile)
.orElse {
ScalaCliBspScope.scalaCliBspRoot(workspace) match {
case Nil => None
case path :: Nil if path.isFile => Some(path.parent)
case path :: Nil =>
scribe.info(s"path: $path")
Some(path)
case _ => Some(workspace)
}
}

def gradleProject: Option[AbsolutePath] = {
val defaultGradlePaths = List(
"settings.gradle",
"settings.gradle.kts",
"build.gradle",
"build.gradle.kts",
)
defaultGradlePaths.exists(workspace.resolve(_).isFile)
searchForBuildTool(root =>
defaultGradlePaths.exists(root.resolve(_).isFile)
)
}
def isMaven: Boolean = workspace.resolve("pom.xml").isFile
def isPants: Boolean = workspace.resolve("pants.ini").isFile
def isBazel: Boolean = workspace.resolve("WORKSPACE").isFile
def isGradle: Boolean = gradleProject.isDefined
def mavenProject: Option[AbsolutePath] = searchForBuildTool(
_.resolve("pom.xml").isFile
)
def isMaven: Boolean = mavenProject.isDefined
def pantsProject: Option[AbsolutePath] = searchForBuildTool(
_.resolve("pants.ini").isFile
)
def isPants: Boolean = pantsProject.isDefined
def bazelProject: Option[AbsolutePath] = searchForBuildTool(
_.resolve("WORKSPACE").isFile
)
def isBazel: Boolean = bazelProject.isDefined

private def searchForBuildTool(
isProjectRoot: AbsolutePath => Boolean
): Option[AbsolutePath] =
if (isProjectRoot(workspace)) Some(workspace)
else
workspace.toNIO
.toFile()
.listFiles()
.collectFirst {
case file
if file.isDirectory &&
!file.getName.startsWith(".") &&
isProjectRoot(AbsolutePath(file.toPath())) =>
AbsolutePath(file.toPath())
}

def allAvailable: List[BuildTool] = {
List(
SbtBuildTool(workspaceVersion = None, userConfig),
GradleBuildTool(userConfig),
MavenBuildTool(userConfig),
MillBuildTool(userConfig),
ScalaCliBuildTool(workspace, userConfig),
SbtBuildTool(workspaceVersion = None, workspace, userConfig),
GradleBuildTool(userConfig, workspace),
MavenBuildTool(userConfig, workspace),
MillBuildTool(userConfig, workspace),
ScalaCliBuildTool(workspace, workspace, userConfig),
)
}

Expand All @@ -111,12 +160,11 @@ final class BuildTools(
def loadSupported(): List[BuildTool] = {
val buf = List.newBuilder[BuildTool]

if (isSbt) buf += SbtBuildTool(workspace, userConfig)
if (isGradle) buf += GradleBuildTool(userConfig)
if (isMaven) buf += MavenBuildTool(userConfig)
if (isMill) buf += MillBuildTool(userConfig)
if (isScalaCli)
buf += ScalaCliBuildTool(workspace, userConfig)
sbtProject.foreach(buf += SbtBuildTool(_, userConfig))
gradleProject.foreach(buf += GradleBuildTool(userConfig, _))
mavenProject.foreach(buf += MavenBuildTool(userConfig, _))
millProject.foreach(buf += MillBuildTool(userConfig, _))
scalaCliProject.foreach(buf += ScalaCliBuildTool(workspace, _, userConfig))

buf.result()
}
Expand All @@ -130,11 +178,11 @@ final class BuildTools(
def isBuildRelated(
path: AbsolutePath
): Option[String] = {
if (isSbt && SbtBuildTool.isSbtRelatedPath(workspace, path))
if (sbtProject.exists(SbtBuildTool.isSbtRelatedPath(_, path)))
Some(SbtBuildTool.name)
else if (isGradle && GradleBuildTool.isGradleRelatedPath(workspace, path))
else if (gradleProject.exists(GradleBuildTool.isGradleRelatedPath(_, path)))
Some(GradleBuildTool.name)
else if (isMaven && MavenBuildTool.isMavenRelatedPath(workspace, path))
else if (mavenProject.exists(MavenBuildTool.isMavenRelatedPath(_, path)))
Some(MavenBuildTool.name)
else if (isMill && MillBuildTool.isMillRelatedPath(path))
Some(MillBuildTool.name)
Expand Down
Loading

0 comments on commit 4653648

Please sign in to comment.