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

Re-use MillBuildRootModule for BSP #2415

Merged
merged 7 commits into from
May 11, 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
59 changes: 44 additions & 15 deletions bsp/worker/src/mill/bsp/worker/MillBuildServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ import mill.define.{Args, Discover, ExternalModule, Module, Segments, Task}
import mill.eval.Evaluator
import mill.main.{BspServerResult, MainModule}
import mill.scalalib.{JavaModule, SemanticDbJavaModule, TestModule}
import mill.scalalib.bsp.{BspModule, JvmBuildTarget, MillBuildModule, ScalaBuildTarget}
import mill.scalalib.bsp.{BspModule, JvmBuildTarget, ScalaBuildTarget}
import mill.scalalib.internal.ModuleUtils
import mill.runner.MillBuildRootModule
import os.Path

import java.io.PrintStream
Expand Down Expand Up @@ -102,15 +103,18 @@ class MillBuildServer(
idToModule match {
case None =>
val modules: Seq[Module] =
ModuleUtils.transitiveModules(evaluator.rootModule) ++ Seq(`mill-build`)
ModuleUtils.transitiveModules(evaluator.rootModule)
val map = modules.collect {
case m: MillBuildModule =>
case m: MillBuildRootModule =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether this is still a singleton. Do we currently project meta-builds to individual synthetic modules? If not, should we?

I think, we should, if possible. To provide a nicer editing experience. On each level we might have different dependencies and settings, so separating them as (synthetic) modules make sense. But if we plan to do that, we should not match simply match on the type here, as we assume hard-coded module segments in the code below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lefou no, it should no longer be a singleton. However, this PR assumes it is, basically ignoring meta-builds

It shouldn't be too hard to make it a non-singleton, we'd just need to move MIllBuildBootstrap over to make it available, and then fish out the MillBuildRootModule from each level.

Copy link
Member Author

@lihaoyi lihaoyi Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sketched out what it would look like handling all the meta-module's MillBuildRootModule, rather than just hardcoding one. But I'll probably need your help to test this and poke and it to get it working, not familiar with the BSP logic at all

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I currently just run vscode (which I'm not using myself regularily) on a recent Mill project and test a bit manually, but that's unsatisfying and way too ineffective. There are some BSP test suites out there which we could use, also a recently renewed one (https:/build-server-protocol/bsp-testkit2), but they all come with no documentation and I simply lack the time to dig deeper how to use/integrate them.

val uri = sanitizeUri(m.millSourcePath)
val id = new BuildTargetIdentifier(uri)
log.debug(s"mill-build segments: ${`mill-build`.millModuleSegments.render}")
for (millBuild <- millBuildRootModules) {
log.debug(s"mill-build segments: ${millBuild.millModuleSegments.render}")
}
(id, m)
case m: BspModule =>
val uri = sanitizeUri(`mill-build`.millSourcePath / m.millModuleSegments.parts)
val uri =
sanitizeUri(millBuildRootModules.head.millSourcePath / m.millModuleSegments.parts)
val id = new BuildTargetIdentifier(uri)
(id, m)
}.toMap
Expand All @@ -124,13 +128,37 @@ class MillBuildServer(
private[MillBuildServer] var modulesToId: Option[Map[BspModule, BuildTargetIdentifier]] = None
}

lazy val `mill-build`: MillBuildModule = {
object `mill-build` extends MillBuildModule {
override protected def projectPath: Path = evaluator.rootModule.millSourcePath
}
`mill-build`
}
lazy val millBuildRootModules: Seq[mill.runner.MillBuildRootModule] = {
val evaluated = new mill.runner.MillBuildBootstrap(
projectRoot = evaluator.rootModule.millSourcePath,
home = os.home,
keepGoing = false,
imports = Nil,
env = Map.empty,
threadCount = None,
targetsAndParams = Seq("resolve", "_"),
prevRunnerState = mill.runner.RunnerState.empty,
logger = evaluator.baseLogger
).evaluate()

val allModules =
evaluated.result.bootstrapModuleOpt.toSeq ++
evaluated.result.frames
.zipWithIndex
// The depth=0 frame is for running user code, not for the build
.drop(1)
.flatMap {
case (f, i) =>
f.classLoaderOpt
.flatMap(
mill.runner.MillBuildBootstrap
.getRootModule(_, i, evaluator.rootModule.millSourcePath)
.toOption
)
}

allModules.collect { case m: mill.runner.MillBuildRootModule => m }
}
def bspModulesById: Map[BuildTargetIdentifier, BspModule] = {
internal.init()
internal.idToModule.get
Expand Down Expand Up @@ -366,7 +394,7 @@ class MillBuildServer(
targetIds = sourcesParams.getTargets.asScala.toSeq,
agg = (items: Seq[SourcesItem]) => new SourcesResult(items.asJava)
) {
case (id, module: MillBuildModule) if clientIsIntelliJ =>
case (id, module: MillBuildRootModule) if clientIsIntelliJ =>
T.task {
val sources = new SourcesItem(
id,
Expand Down Expand Up @@ -560,7 +588,7 @@ class MillBuildServer(
import state._

val modules = bspModulesById.values.toSeq.collect { case m: JavaModule => m }
val millBuildTargetId = bspIdByModule(`mill-build`)
val millBuildTargetIds = millBuildRootModules.map(bspIdByModule(_)).toSet

val params = TaskParameters.fromTestParams(testParams)
val argsMap =
Expand All @@ -580,7 +608,7 @@ class MillBuildServer(
}

val overallStatusCode = testParams.getTargets.asScala
.filter(_ != millBuildTargetId)
.filter(millBuildTargetIds.contains)
.foldLeft(StatusCode.OK) { (overallStatusCode, targetId) =>
bspModulesById(targetId) match {
case testModule: TestModule =>
Expand Down Expand Up @@ -646,8 +674,9 @@ class MillBuildServer(
completable(s"buildTargetCleanCache ${cleanCacheParams}") { state =>
import state._

val targetIds = millBuildRootModules.map(_.buildTargetId)
val (msg, cleaned) =
cleanCacheParams.getTargets.asScala.filter(_ != `mill-build`.buildTargetId).foldLeft((
cleanCacheParams.getTargets.asScala.filter(targetIds.contains).foldLeft((
"",
true
)) {
Expand Down
2 changes: 1 addition & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,7 @@ object bsp extends MillModule with BuildInfo {
}

object worker extends MillInternalModule with BuildInfo {
override def compileModuleDeps = Seq(bsp, scalalib, testrunner)
override def compileModuleDeps = Seq(bsp, scalalib, testrunner, runner)
override def ivyDeps = Agg(
Deps.bsp4j,
Deps.sbtTestInterface
Expand Down
2 changes: 1 addition & 1 deletion contrib/bloop/src/mill/contrib/bloop/BloopFormats.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package mill.contrib.bloop

import bloop.config.{Config => BloopConfig}
import _root_.bloop.config.{Config => BloopConfig}
import upickle.default.{ReadWriter, macroRW, readwriter}

object BloopFormats {
Expand Down
7 changes: 5 additions & 2 deletions contrib/bloop/src/mill/contrib/bloop/BloopImpl.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package mill.contrib.bloop

import bloop.config.{Config => BloopConfig, Tag => BloopTag}
import _root_.bloop.config.{Config => BloopConfig, Tag => BloopTag}
import mill._
import mill.api.Result
import mill.define.{Module => MillModule, ExternalModule, Discover}
Expand Down Expand Up @@ -144,7 +144,10 @@ class BloopImpl(ev: () => Evaluator, wd: os.Path) extends ExternalModule { outer
Result.Success(sources.toMap)
}

protected def name(m: JavaModule) = ModuleUtils.moduleDisplayName(m)
protected def name(m: JavaModule) = ModuleUtils.moduleDisplayName(m) match {
case "" => "root-module"
case n => n
}

protected def bloopConfigPath(module: JavaModule): os.Path =
bloopDir / s"${name(module)}.json"
Expand Down
132 changes: 79 additions & 53 deletions runner/src/mill/runner/MillBuildBootstrap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,25 @@ import java.net.URLClassLoader
@internal
class MillBuildBootstrap(
projectRoot: os.Path,
config: MillCliConfig,
home: os.Path,
keepGoing: Boolean,
imports: Seq[String],
env: Map[String, String],
threadCount: Option[Int],
targetsAndParams: Seq[String],
prevRunnerState: RunnerState,
logger: ColorLogger
) {
import MillBuildBootstrap._

val millBootClasspath = MillBuildBootstrap.prepareMillBootClasspath(projectRoot / "out")
val millBootClasspath = prepareMillBootClasspath(projectRoot / "out")

def evaluate(): Watching.Result[RunnerState] = {
val runnerState = evaluateRec(0)

for ((frame, depth) <- runnerState.frames.zipWithIndex) {
os.write.over(
recOut(depth) / "mill-runner-state.json",
recOut(projectRoot, depth) / "mill-runner-state.json",
upickle.default.write(frame.loggedData, indent = 4),
createFolders = true
)
Expand All @@ -56,31 +59,26 @@ class MillBuildBootstrap(
)
}

def getRootModule0(runClassLoader: URLClassLoader): RootModule = {
val cls = runClassLoader.loadClass("millbuild.build$")
cls.getField("MODULE$").get(cls).asInstanceOf[RootModule]
}

def evaluateRec(depth: Int): RunnerState = {
// println(s"+evaluateRec($depth) " + recRoot(depth))
// println(s"+evaluateRec($depth) " + recRoot(projectRoot, depth))
val prevFrameOpt = prevRunnerState.frames.lift(depth)
val prevOuterFrameOpt = prevRunnerState.frames.lift(depth - 1)

val nestedRunnerState =
if (!os.exists(recRoot(depth) / "build.sc")) {
if (!os.exists(recRoot(projectRoot, depth) / "build.sc")) {
if (depth == 0) {
RunnerState(None, Nil, Some("build.sc file not found. Are you in a Mill project folder?"))
} else {

val bootstrapModule =
new MillBuildRootModule.BootstrapModule(
projectRoot,
recRoot(depth),
recRoot(projectRoot, depth),
millBootClasspath,
config.imports.collect { case s"ivy:$rest" => rest }
imports.collect { case s"ivy:$rest" => rest }
)(
mill.main.RootModule.Info(
recRoot(depth),
recRoot(projectRoot, depth),
Discover[MillBuildRootModule.BootstrapModule]
)
)
Expand All @@ -93,40 +91,22 @@ class MillBuildBootstrap(
val res = if (nestedRunnerState.errorOpt.isDefined) {
nestedRunnerState.add(errorOpt = nestedRunnerState.errorOpt)
} else {
val rootModule0 = nestedRunnerState.frames.headOption match {
case None => nestedRunnerState.bootstrapModuleOpt.get
case Some(nestedFrame) => getRootModule0(nestedFrame.classLoaderOpt.get)
}

val childRootModules: Seq[RootModule] = rootModule0
.millInternal
.reflectNestedObjects[RootModule]()

val rootModuleOrErr = childRootModules match {
case Seq() => Right(rootModule0)
case Seq(child) =>
val invalidChildModules = rootModule0.millModuleDirectChildren.filter(_ ne child)
if (invalidChildModules.isEmpty) Right(child)
else Left(
// We can't get use `child.toString` here, because as a RootModule
// it's segments are empty and it's toString is ""
s"RootModule ${child.getClass.getSimpleName} cannot have other " +
s"modules defined outside of it: ${invalidChildModules.mkString(",")}"
val validatedRootModuleOrErr = nestedRunnerState.frames.headOption match {
case None =>
getChildRootModule(
nestedRunnerState.bootstrapModuleOpt.get,
depth,
projectRoot
)

case multiple =>
Left(
s"Only one RootModule can be defined in a build, not " +
s"${multiple.size}: ${multiple.map(_.getClass.getName).mkString(",")}"
case Some(nestedFrame) =>
getRootModule(
nestedFrame.classLoaderOpt.get,
depth,
projectRoot
)
}

val validatedRootModuleOrErr = rootModuleOrErr.filterOrElse(
rootModule =>
depth == 0 || rootModule.isInstanceOf[MillBuildRootModule],
s"Root module in ${recRoot(depth).relativeTo(projectRoot)}/build.sc must be of ${classOf[mill.runner.MillBuildRootModule]}"
)

validatedRootModuleOrErr match {
case Left(err) => nestedRunnerState.add(errorOpt = Some(err))
case Right(rootModule) =>
Expand Down Expand Up @@ -158,7 +138,7 @@ class MillBuildBootstrap(
else processFinalTargets(nestedRunnerState, rootModule, evaluator)
}
}
// println(s"-evaluateRec($depth) " + recRoot(depth))
// println(s"-evaluateRec($depth) " + recRoot(projectRoot, depth))
res
}

Expand All @@ -179,8 +159,7 @@ class MillBuildBootstrap(
prevFrameOpt: Option[RunnerState.Frame],
prevOuterFrameOpt: Option[RunnerState.Frame]
): RunnerState = {

MillBuildBootstrap.evaluateWithWatches(
evaluateWithWatches(
rootModule,
evaluator,
Seq("{runClasspath,scriptImportGraph}")
Expand Down Expand Up @@ -258,7 +237,7 @@ class MillBuildBootstrap(
): RunnerState = {

val (evaled, evalWatched, moduleWatches) =
MillBuildBootstrap.evaluateWithWatches(rootModule, evaluator, targetsAndParams)
evaluateWithWatches(rootModule, evaluator, targetsAndParams)

val evalState = RunnerState.Frame(
evaluator.workerCache.toMap,
Expand All @@ -285,23 +264,20 @@ class MillBuildBootstrap(
else "[" + (Seq.fill(depth - 1)("mill-build") ++ Seq("build.sc")).mkString("/") + "] "

mill.eval.EvaluatorImpl(
config.home,
recOut(depth),
recOut(depth),
home,
recOut(projectRoot, depth),
recOut(projectRoot, depth),
rootModule,
PrefixLogger(logger, "", tickerContext = bootLogPrefix),
millClassloaderSigHash,
workerCache = workerCache.to(collection.mutable.Map),
env = env,
failFast = !config.keepGoing.value,
failFast = !keepGoing,
threadCount = threadCount,
scriptImportGraph = scriptImportGraph
)
}

def recRoot(depth: Int) = projectRoot / Seq.fill(depth)("mill-build")
def recOut(depth: Int) = projectRoot / "out" / Seq.fill(depth)("mill-build")

}

@internal
Expand Down Expand Up @@ -354,4 +330,54 @@ object MillBuildBootstrap {
}
}
}

def getRootModule(
runClassLoader: URLClassLoader,
depth: Int,
projectRoot: os.Path
): Either[String, RootModule] = {
val cls = runClassLoader.loadClass("millbuild.build$")
val rootModule0 = cls.getField("MODULE$").get(cls).asInstanceOf[RootModule]
getChildRootModule(rootModule0, depth, projectRoot)
}

def getChildRootModule(rootModule0: RootModule, depth: Int, projectRoot: os.Path) = {

val childRootModules: Seq[RootModule] = rootModule0
.millInternal
.reflectNestedObjects[RootModule]()

val rootModuleOrErr = childRootModules match {
case Seq() => Right(rootModule0)
case Seq(child) =>
val invalidChildModules = rootModule0.millModuleDirectChildren.filter(_ ne child)
if (invalidChildModules.isEmpty) Right(child)
else Left(
// We can't get use `child.toString` here, because as a RootModule
// it's segments are empty and it's toString is ""
s"RootModule ${child.getClass.getSimpleName} cannot have other " +
s"modules defined outside of it: ${invalidChildModules.mkString(",")}"
)

case multiple =>
Left(
s"Only one RootModule can be defined in a build, not " +
s"${multiple.size}: ${multiple.map(_.getClass.getName).mkString(",")}"
)
}

rootModuleOrErr.filterOrElse(
rootModule =>
depth == 0 || rootModule.isInstanceOf[mill.runner.MillBuildRootModule],
s"Root module in ${recRoot(projectRoot, depth).relativeTo(projectRoot)}/build.sc must be of ${classOf[MillBuildRootModule]}"
)
}

def recRoot(projectRoot: os.Path, depth: Int) = {
projectRoot / Seq.fill(depth)("mill-build")
}

def recOut(projectRoot: os.Path, depth: Int) = {
projectRoot / "out" / Seq.fill(depth)("mill-build")
}
}
3 changes: 3 additions & 0 deletions runner/src/mill/runner/MillBuildRootModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ class MillBuildRootModule()(implicit
def lineNumberPluginClasspath: T[Agg[PathRef]] = T {
millProjectModule("mill-runner-linenumbers", repositoriesTask())
}

/** Used in BSP IntelliJ, which can only work with directories */
def dummySources: Sources = T.sources(T.dest)
}

object MillBuildRootModule {
Expand Down
Loading