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: auto detect project root #5576

Merged
merged 6 commits into from
Sep 22, 2023
Merged
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
@@ -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,
kasiaMarek marked this conversation as resolved.
Show resolved Hide resolved
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
Loading