summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rwxr-xr-xbuild.sc14
-rw-r--r--contrib/bsp/readme.md43
-rw-r--r--contrib/bsp/src/mill/contrib/BSP.scala137
-rw-r--r--contrib/bsp/src/mill/contrib/bsp/BspLoggedReporter.scala138
-rw-r--r--contrib/bsp/src/mill/contrib/bsp/BspTestReporter.scala117
-rw-r--r--contrib/bsp/src/mill/contrib/bsp/MillBspLogger.scala55
-rw-r--r--contrib/bsp/src/mill/contrib/bsp/MillBuildServer.scala574
-rw-r--r--contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala297
-rw-r--r--contrib/bsp/src/mill/contrib/bsp/TaskParameters.scala127
-rw-r--r--main/api/src/mill/api/BuildReporter.scala97
-rw-r--r--main/api/src/mill/api/Ctx.scala5
-rw-r--r--main/core/src/eval/Evaluator.scala65
-rw-r--r--scalajslib/src/ScalaJSModule.scala3
-rw-r--r--scalalib/api/src/ZincWorkerApi.scala11
-rw-r--r--scalalib/src/JavaModule.scala9
-rw-r--r--scalalib/src/ScalaModule.scala4
-rw-r--r--scalalib/src/TestRunner.scala28
-rw-r--r--scalalib/worker/src/ZincWorkerImpl.scala102
-rw-r--r--scalanativelib/src/ScalaNativeModule.scala3
20 files changed, 1775 insertions, 60 deletions
diff --git a/.gitignore b/.gitignore
index d79d325d..6c7f0baa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,9 @@ output/
out/
/.bloop/
/.metals/
+contrib/bsp/mill-external-bs
+contrib/bsp/mill-out-bs
+mill.iml
+.bsp/
+bsp.log
+contrib/bsp/test/ \ No newline at end of file
diff --git a/build.sc b/build.sc
index abf9b9b7..bd06c10f 100755
--- a/build.sc
+++ b/build.sc
@@ -53,6 +53,7 @@ object Deps {
val upickle = ivy"com.lihaoyi::upickle:0.7.1"
val utest = ivy"com.lihaoyi::utest:0.6.4"
val zinc = ivy"org.scala-sbt::zinc:1.2.5"
+ val bsp = ivy"ch.epfl.scala:bsp4j:2.0.0-M4"
}
trait MillPublishModule extends PublishModule{
@@ -439,6 +440,17 @@ object contrib extends MillModule {
)
def testArgs = T(scalanativelib.testArgs())
}
+
+ object bsp extends MillModule {
+
+ def moduleDeps = Seq(scalalib, scalajslib, main, scalanativelib)
+ def ivyDeps = Agg(
+ Deps.bsp,
+ Deps.ujsonCirce,
+ Deps.sbtTestInterface
+ )
+ }
+
}
@@ -561,7 +573,7 @@ def launcherScript(shellJvmArgs: Seq[String],
}
object dev extends MillModule{
- def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, contrib.scalapblib, contrib.tut, contrib.scoverage)
+ def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, contrib.scalapblib, contrib.tut, contrib.scoverage, contrib.bsp)
def forkArgs =
diff --git a/contrib/bsp/readme.md b/contrib/bsp/readme.md
new file mode 100644
index 00000000..c9557e3a
--- /dev/null
+++ b/contrib/bsp/readme.md
@@ -0,0 +1,43 @@
+# Build Server Protocol support for mill
+
+The contrib.bsp module was created in order to integrate the Mill build tool
+with IntelliJ IDEA via the Build Server Protocol (BSP). It implements most of
+the server side functionality described in BSP, and can therefore connect to a
+BSP client, including the one behind IntelliJ IDEA. This allows a lot of mill
+tasks to be executed from the IDE.
+
+# Importing an existing mill project in IntelliJ via BSP
+
+1) Clone the mill git repo
+2) Publish your mill version locally with `ci/publish-contrib`
+3) Run the following command in the working directory of your project:
+
+ `~/mill-release -i mill.contrib.BSP/install`
+
+ This should create a `.bsp/` directory inside your working directory,
+ containing a BSP connection file that clients can use to start the
+ BSP server for Mill.
+
+ This command should be ran whenever you change the version of mill that
+ you use.
+
+ 4) Now you can use IntelliJ to import your project from existing sources
+ via bsp ( currently available in the nightly release ). Note: It might
+ take a few minutes to import a project the very first time.
+
+After the bsp support module would be published, it should be enough to:
+
+1) Install mill
+2) Add the following import statement in the build.sc of your project:
+
+ `import $ivy.com.lihaoyi::mill-contrib-bsp:$OFFICIAL_MILL_VERSION`
+
+3) Run the following command in the working directory of your project:
+
+ `mill -i mill.contrib.BSP/install`
+
+## Known Issues:
+
+- Sometimes build from IntelliJ might fail due to a NoClassDefFoundException
+being thrown during the evaluation of tasks, a bug not easy to reproduce.
+In this case it is recommended to refresh the bsp project.
diff --git a/contrib/bsp/src/mill/contrib/BSP.scala b/contrib/bsp/src/mill/contrib/BSP.scala
new file mode 100644
index 00000000..f214ed9a
--- /dev/null
+++ b/contrib/bsp/src/mill/contrib/BSP.scala
@@ -0,0 +1,137 @@
+package mill.contrib
+
+import java.io.PrintWriter
+import java.nio.file.FileAlreadyExistsException
+import java.util.concurrent.Executors
+
+import ch.epfl.scala.bsp4j._
+import mill._
+import mill.define.{Command, Discover, ExternalModule}
+import mill.eval.Evaluator
+import org.eclipse.lsp4j.jsonrpc.Launcher
+import upickle.default._
+
+import scala.collection.JavaConverters._
+import scala.concurrent.CancellationException
+
+case class BspConfigJson(name: String,
+ argv: Seq[String],
+ version: String,
+ bspVersion: String,
+ languages: Seq[String])
+ extends BspConnectionDetails(name, argv.asJava, version, bspVersion, languages.asJava) {
+}
+
+object BspConfigJson {
+ implicit val rw: ReadWriter[BspConfigJson] = macroRW
+}
+
+object BSP extends ExternalModule {
+
+ implicit def millScoptEvaluatorReads[T] = new mill.main.EvaluatorScopt[T]()
+
+ lazy val millDiscover: Discover[BSP.this.type] = Discover[this.type]
+ val version = "1.0.0"
+ val bspProtocolVersion = "2.0.0"
+ val languages = List("scala", "java")
+
+ /**
+ * Installs the mill-bsp server. It creates a json file
+ * with connection details in the ./.bsp directory for
+ * a potential client to find.
+ *
+ * If a .bsp folder with a connection file already
+ * exists in the working directory, it will be
+ * overwritten and a corresponding message will be displayed
+ * in stdout.
+ *
+ * If the creation of the .bsp folder fails due to any other
+ * reason, the message and stacktrace of the exception will be
+ * printed to stdout.
+ *
+ */
+ def install(ev: Evaluator): Command[Unit] = T.command {
+ val bspDirectory = os.pwd / ".bsp"
+ if (!os.exists(bspDirectory)) os.makeDir.all(bspDirectory)
+ try {
+ os.write(bspDirectory / "mill.json", createBspConnectionJson())
+ } catch {
+ case e: FileAlreadyExistsException =>
+ println("The bsp connection json file probably exists already - will be overwritten")
+ os.remove(bspDirectory / "mill.json")
+ os.write(bspDirectory / "mill.json", createBspConnectionJson())
+ case e: Exception =>
+ println("An exception occurred while installing mill-bsp")
+ e.printStackTrace()
+ }
+
+ }
+
+ // creates a Json with the BSP connection details
+ def createBspConnectionJson(): String = {
+ val millPath = scala.sys.props.get("MILL_CLASSPATH").getOrElse(System.getProperty("MILL_CLASSPATH"))
+ val millVersion = scala.sys.props.get("MILL_VERSION").getOrElse(System.getProperty("MILL_VERSION"))
+ write(BspConfigJson("mill-bsp",
+ List(whichJava,
+ s"-DMILL_CLASSPATH=$millPath",
+ s"-DMILL_VERSION=$millVersion",
+ "-Djna.nosys=true",
+ "-cp",
+ millPath,
+ "mill.MillMain",
+ "mill.contrib.BSP/start"),
+ version,
+ bspProtocolVersion,
+ languages))
+ }
+
+ // computes the path to the java executable
+ def whichJava: String = {
+ if (scala.sys.props.contains("JAVA_HOME")) scala.sys.props("JAVA_HOME") else "java"
+ }
+
+ /**
+ * Computes a mill command which starts the mill-bsp
+ * server and establishes connection to client. Waits
+ * until a client connects and ends the connection
+ * after the client sent an "exit" notification
+ *
+ * @param ev Environment, used by mill to evaluate commands
+ * @return: mill.Command which executes the starting of the
+ * server
+ */
+ def start(ev: Evaluator): Command[Unit] = T.command {
+ val eval = new Evaluator(ev.home, ev.outPath, ev.externalOutPath, ev.rootModule, ev.log, ev.classLoaderSig,
+ ev.workerCache, ev.env, false)
+ val millServer = new mill.contrib.bsp.MillBuildServer(eval, bspProtocolVersion, version, languages)
+ val executor = Executors.newCachedThreadPool()
+
+ val stdin = System.in
+ val stdout = System.out
+ try {
+ val launcher = new Launcher.Builder[BuildClient]()
+ .setOutput(stdout)
+ .setInput(stdin)
+ .setLocalService(millServer)
+ .setRemoteInterface(classOf[BuildClient]).
+ traceMessages(new PrintWriter((os.pwd / "bsp.log").toIO))
+ .setExecutorService(executor)
+ .create()
+ millServer.onConnectWithClient(launcher.getRemoteProxy)
+ val listening = launcher.startListening()
+ millServer.cancelator = () => listening.cancel(true)
+ val voidFuture = listening.get()
+ } catch {
+ case _: CancellationException => System.err.println("The mill server was shut down.")
+ case e: Exception =>
+ System.err.println("An exception occured while connecting to the client.")
+ System.err.println("Cause: " + e.getCause)
+ System.err.println("Message: " + e.getMessage)
+ System.err.println("Exception class: " + e.getClass)
+ System.err.println("Stack Trace: " + e.getStackTrace)
+ } finally {
+ System.err.println("Shutting down executor")
+ executor.shutdown()
+ }
+ }
+}
diff --git a/contrib/bsp/src/mill/contrib/bsp/BspLoggedReporter.scala b/contrib/bsp/src/mill/contrib/bsp/BspLoggedReporter.scala
new file mode 100644
index 00000000..d1b9a66b
--- /dev/null
+++ b/contrib/bsp/src/mill/contrib/bsp/BspLoggedReporter.scala
@@ -0,0 +1,138 @@
+package mill.contrib.bsp
+
+import java.io.File
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicInteger
+
+import ch.epfl.scala.bsp4j._
+import ch.epfl.scala.{bsp4j => bsp}
+import mill.api.{BuildProblemReporter, Problem}
+
+import scala.collection.JavaConverters._
+import scala.collection.concurrent
+import scala.language.implicitConversions
+
+/**
+ * Specialized reporter that sends compilation diagnostics
+ * for each problem it logs, either as information, warning or
+ * error as well as task finish notifications of type `compile-report`.
+ *
+ * @param client the client to send diagnostics to
+ * @param targetId the target id of the target whose compilation
+ * the diagnostics are related to
+ * @param taskId a unique id of the compilation task of the target
+ * specified by `targetId`
+ * @param compilationOriginId optional origin id the client assigned to
+ * the compilation request. Needs to be sent
+ * back as part of the published diagnostics
+ * as well as compile report
+ */
+class BspLoggedReporter(client: bsp.BuildClient,
+ targetId: BuildTargetIdentifier,
+ taskId: TaskId,
+ compilationOriginId: Option[String]) extends BuildProblemReporter {
+
+ var errors = new AtomicInteger(0)
+ var warnings = new AtomicInteger(0)
+ var infos = new AtomicInteger(0)
+ var diagnosticMap: concurrent.Map[TextDocumentIdentifier, bsp.PublishDiagnosticsParams] =
+ new ConcurrentHashMap[TextDocumentIdentifier, bsp.PublishDiagnosticsParams]().asScala
+
+ override def logError(problem: Problem): Unit = {
+ client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId))
+ errors.incrementAndGet()
+ }
+
+ override def logInfo(problem: Problem): Unit = {
+ client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId))
+ infos.incrementAndGet()
+ }
+
+ // Obtains the parameters for sending diagnostics about the given Problem ( as well as
+ // about all previous problems generated for the same text file ) related to the specified
+ // targetId, incorporating the given optional originId ( generated by the client for the
+ // compile request )
+ //TODO: document that if the problem is a general information without a text document
+ // associated to it, then the document field of the diagnostic is set to the uri of the target
+ private[this] def getDiagnostics(problem: Problem, targetId: bsp.BuildTargetIdentifier, originId: Option[String]):
+ bsp.PublishDiagnosticsParams = {
+ val diagnostic = getSingleDiagnostic(problem)
+ val sourceFile = problem.position.sourceFile
+ val textDocument = new TextDocumentIdentifier(
+ sourceFile.getOrElse(None) match {
+ case None => targetId.getUri
+ case f: File => f.toURI.toString
+ })
+ val params = new bsp.PublishDiagnosticsParams(textDocument,
+ targetId,
+ appendDiagnostics(textDocument,
+ diagnostic).asJava,
+ true)
+
+ if (originId.nonEmpty) {
+ params.setOriginId(originId.get)
+ }
+ diagnosticMap.put(textDocument, params)
+ params
+ }
+
+ // Update the published diagnostics for the fiven text file by
+ // adding the recently computed diagnostic to the list of
+ // all previous diagnostics generated for the same file.
+ private[this] def appendDiagnostics(textDocument: TextDocumentIdentifier,
+ currentDiagnostic: Diagnostic): List[Diagnostic] = {
+ diagnosticMap.putIfAbsent(textDocument, new bsp.PublishDiagnosticsParams(
+ textDocument,
+ targetId,
+ List.empty[Diagnostic].asJava, true))
+ diagnosticMap(textDocument).getDiagnostics.asScala.toList ++ List(currentDiagnostic)
+ }
+
+ // Computes the diagnostic related to the given Problem
+ private[this] def getSingleDiagnostic(problem: Problem): Diagnostic = {
+ val pos = problem.position
+ val i: Integer = pos.startLine.orElse(pos.line).getOrElse[Int](0)
+ println(i)
+ val start = new bsp.Position(
+ pos.startLine.orElse(pos.line).getOrElse[Int](0),
+ pos.startOffset.orElse(pos.offset).getOrElse[Int](0)
+ )
+ val end = new bsp.Position(
+ pos.endLine.orElse(pos.line).getOrElse[Int](start.getLine.intValue()),
+ pos.endOffset.orElse(pos.offset).getOrElse[Int](start.getCharacter.intValue()))
+ val diagnostic = new bsp.Diagnostic(new bsp.Range(start, end), problem.message)
+ diagnostic.setCode(pos.lineContent)
+ diagnostic.setSource("compiler from mill")
+ diagnostic.setSeverity(problem.severity match {
+ case mill.api.Info => bsp.DiagnosticSeverity.INFORMATION
+ case mill.api.Error => bsp.DiagnosticSeverity.ERROR
+ case mill.api.Warn => bsp.DiagnosticSeverity.WARNING
+ }
+ )
+ diagnostic
+ }
+
+ override def logWarning(problem: Problem): Unit = {
+ client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId))
+ warnings.incrementAndGet()
+ }
+
+ override def printSummary(): Unit = {
+ val taskFinishParams = new TaskFinishParams(taskId, getStatusCode)
+ taskFinishParams.setEventTime(System.currentTimeMillis())
+ taskFinishParams.setMessage("Finished compiling target: " + targetId.getUri)
+ taskFinishParams.setDataKind("compile-report")
+ val compileReport = new CompileReport(targetId, errors.get, warnings.get)
+ compilationOriginId match {
+ case Some(id) => compileReport.setOriginId(id)
+ case None =>
+ }
+ taskFinishParams.setData(compileReport)
+ client.onBuildTaskFinish(taskFinishParams)
+ }
+
+ // Compute the compilation status code
+ private[this] def getStatusCode: StatusCode = {
+ if (errors.get > 0) StatusCode.ERROR else StatusCode.OK
+ }
+}
diff --git a/contrib/bsp/src/mill/contrib/bsp/BspTestReporter.scala b/contrib/bsp/src/mill/contrib/bsp/BspTestReporter.scala
new file mode 100644
index 00000000..b49c0b7c
--- /dev/null
+++ b/contrib/bsp/src/mill/contrib/bsp/BspTestReporter.scala
@@ -0,0 +1,117 @@
+package mill.contrib.bsp
+
+import java.io.{PrintWriter, StringWriter}
+
+import ch.epfl.scala.bsp4j._
+import mill.api.TestReporter
+import sbt.testing._
+
+
+/**
+ * Context class for BSP, specialized for sending `task-start` and
+ * `task-finish` notifications for every test being ran.
+ *
+ * @param client The client to send notifications to
+ * @param targetId The targetId of the BSP target for which
+ * the test request is being processed
+ * @param taskId The unique taskId associated with the
+ * test task that will trigger this reporter
+ * to log testing events.
+ * @param arguments compilation arguments as part of the BSP context,
+ * in case special arguments need to be passed to
+ * the compiler before running the test task.
+ */
+class BspTestReporter(client: BuildClient,
+ targetId: BuildTargetIdentifier,
+ taskId: TaskId,
+ arguments: Seq[String]) extends TestReporter {
+
+ var passed = 0
+ var failed = 0
+ var cancelled = 0
+ var ignored = 0
+ var skipped = 0
+ var totalTime: Long = 0
+
+ override def logStart(event: Event): Unit = {
+ val taskStartParams = new TaskStartParams(taskId)
+ taskStartParams.setEventTime(System.currentTimeMillis())
+ taskStartParams.setDataKind(TaskDataKind.TEST_START)
+ taskStartParams.setData(new TestStart(getDisplayName(event)))
+ taskStartParams.setMessage("Starting running: " + getDisplayName(event))
+ client.onBuildTaskStart(taskStartParams)
+ }
+
+ // Compute the display name of the test / test suite
+ // to which the given event relates
+ private[this] def getDisplayName(e: Event): String = {
+ e.selector() match {
+ case s: NestedSuiteSelector => s.suiteId()
+ case s: NestedTestSelector => s.suiteId() + "." + s.testName()
+ case s: SuiteSelector => s.toString
+ case s: TestSelector => s.testName()
+ case s: TestWildcardSelector => s.testWildcard()
+ }
+ }
+
+ override def logFinish(event: Event): Unit = {
+ totalTime += event.duration()
+ val taskFinishParams = new TaskFinishParams(taskId,
+ event.status() match {
+ case sbt.testing.Status.Canceled => StatusCode.CANCELLED
+ case sbt.testing.Status.Error => StatusCode.ERROR
+ case default => StatusCode.OK
+ })
+ val status = event.status match {
+ case sbt.testing.Status.Success =>
+ passed += 1
+ TestStatus.PASSED
+ case sbt.testing.Status.Canceled =>
+ cancelled += 1
+ TestStatus.CANCELLED
+ case sbt.testing.Status.Error =>
+ failed += 1
+ TestStatus.FAILED
+ case sbt.testing.Status.Failure =>
+ failed += 1
+ TestStatus.FAILED
+ case sbt.testing.Status.Ignored =>
+ ignored += 1
+ TestStatus.IGNORED
+ case sbt.testing.Status.Skipped =>
+ skipped += 1
+ TestStatus.SKIPPED
+ case sbt.testing.Status.Pending =>
+ skipped += 1
+ TestStatus.SKIPPED //TODO: what to do here
+ }
+
+ taskFinishParams.setDataKind(TaskDataKind.TEST_FINISH)
+ taskFinishParams.setData({
+ val testFinish = new TestFinish(getDisplayName(event), status)
+ if (event.throwable.isDefined)
+ testFinish.setMessage(throwableToString(event.throwable().get()))
+ testFinish
+ })
+ taskFinishParams.setEventTime(System.currentTimeMillis())
+ client.onBuildTaskFinish(taskFinishParams)
+ }
+
+ private def throwableToString(t: Throwable): String = {
+ val sw = new StringWriter
+ val pw = new PrintWriter(sw)
+ t.printStackTrace(pw)
+ sw.toString
+ }
+
+ // Compute the test report data structure that will go into
+ // the task finish notification after all tests are ran.
+ def getTestReport: TestReport = {
+ val report = new TestReport(targetId, passed, failed, ignored, cancelled, skipped)
+ report.setTime(totalTime)
+ report
+ }
+
+}
+
+case class TestException(stackTrace: String, message: String, exClass: String)
diff --git a/contrib/bsp/src/mill/contrib/bsp/MillBspLogger.scala b/contrib/bsp/src/mill/contrib/bsp/MillBspLogger.scala
new file mode 100644
index 00000000..12638bed
--- /dev/null
+++ b/contrib/bsp/src/mill/contrib/bsp/MillBspLogger.scala
@@ -0,0 +1,55 @@
+package mill.contrib.bsp
+
+import ch.epfl.scala.bsp4j._
+import mill.api.Logger
+import mill.util.ProxyLogger
+
+
+/**
+ * BSP-specialized logger class which sends `task-progress`
+ * notifications ( upon the invocation of the `ticker` method ) and
+ * `show-message` notifications ( for each error or information
+ * being logged ).
+ *
+ * @param client the client to send notifications to, also the
+ * client that initiated a request which triggered
+ * a mill task evaluation
+ * @param taskId unique ID of the task being evaluated
+ * @param logger the logger to which the messages received by this
+ * MillBspLogger are being redirected
+ */
+class MillBspLogger(client: BuildClient, taskId: Int, logger: Logger) extends ProxyLogger(logger) {
+
+ override def ticker(s: String): Unit = {
+ try {
+ val progressString = s.split(" ")(0)
+ val progress = progressString.substring(1, progressString.length - 1).split("/")
+ val params = new TaskProgressParams(new TaskId(taskId.toString))
+ params.setEventTime(System.currentTimeMillis())
+ params.setMessage(s)
+ params.setUnit(s.split(" ")(1))
+ params.setProgress(progress(0).toLong)
+ params.setTotal(progress(1).toLong)
+ client.onBuildTaskProgress(params)
+ super.ticker(s)
+ } catch {
+ case e: Exception =>
+ }
+ }
+
+ override def error(s: String): Unit = {
+ super.error(s)
+ client.onBuildShowMessage(new ShowMessageParams(MessageType.ERROR, s))
+ }
+
+ override def info(s: String): Unit = {
+ super.info(s)
+ client.onBuildShowMessage(new ShowMessageParams(MessageType.INFORMATION, s))
+ }
+
+ override def debug(s: String): Unit = {
+ super.debug(s)
+ client.onBuildShowMessage(new ShowMessageParams(MessageType.LOG, s))
+ }
+
+}
diff --git a/contrib/bsp/src/mill/contrib/bsp/MillBuildServer.scala b/contrib/bsp/src/mill/contrib/bsp/MillBuildServer.scala
new file mode 100644
index 00000000..6db85c2c
--- /dev/null
+++ b/contrib/bsp/src/mill/contrib/bsp/MillBuildServer.scala
@@ -0,0 +1,574 @@
+package mill.contrib.bsp
+
+import java.util.concurrent.CompletableFuture
+
+import ch.epfl.scala.bsp4j._
+import com.google.gson.JsonObject
+import mill.api.Result.{Skipped, Success}
+import mill.api.{BuildProblemReporter, DummyTestReporter, Result, Strict}
+import mill.contrib.bsp.ModuleUtils._
+import mill.define.Segment.Label
+import mill.define.{Discover, ExternalModule}
+import mill.eval.Evaluator
+import mill.main.{EvaluatorScopt, MainModule}
+import mill.modules.Jvm
+import mill.scalalib.Lib.discoverTests
+import mill.scalalib._
+import mill.scalalib.api.CompilationResult
+import mill.util.{Ctx, DummyLogger}
+import mill.{scalalib, _}
+import os.Path
+
+import scala.collection.JavaConverters._
+
+
+class MillBuildServer(evaluator: Evaluator,
+ _bspVersion: String,
+ serverVersion: String,
+ languages: List[String]) extends ExternalModule with BuildServer with ScalaBuildServer {
+
+ implicit def millScoptEvaluatorReads[T]: EvaluatorScopt[T] = new mill.main.EvaluatorScopt[T]()
+
+ lazy val millDiscover: Discover[MillBuildServer.this.type] = Discover[this.type]
+ val bspVersion: String = _bspVersion
+ val supportedLanguages: List[String] = languages
+ val millServerVersion: String = serverVersion
+ val millEvaluator: Evaluator = evaluator
+ val ctx: Ctx.Log with Ctx.Home = new Ctx.Log with Ctx.Home {
+ val log: DummyLogger.type = mill.util.DummyLogger
+ val home: Path = os.pwd
+ }
+ var cancelator: () => Unit = () => ()
+ var rootModule: JavaModule = ModuleUtils.getRootJavaModule(evaluator.rootModule)
+ var millModules: Seq[JavaModule] = getMillModules(millEvaluator)
+ var client: BuildClient = _
+ var moduleToTargetId: Predef.Map[JavaModule, BuildTargetIdentifier] = ModuleUtils.getModuleTargetIdMap(
+ millModules,
+ evaluator
+ )
+ var targetIdToModule: Predef.Map[BuildTargetIdentifier, JavaModule] = targetToModule(moduleToTargetId)
+ var moduleToTarget: Predef.Map[JavaModule, BuildTarget] =
+ ModuleUtils.millModulesToBspTargets(millModules, rootModule, evaluator, List("scala", "java"))
+ var moduleCodeToTargetId: Predef.Map[Int, BuildTargetIdentifier] =
+ for ((targetId, module) <- targetIdToModule) yield (module.hashCode(), targetId)
+ var initialized = false
+ var clientInitialized = false
+
+ override def onConnectWithClient(server: BuildClient): Unit =
+ client = server
+
+ override def buildInitialize(params: InitializeBuildParams): CompletableFuture[InitializeBuildResult] = {
+
+ val capabilities = new BuildServerCapabilities
+ capabilities.setCompileProvider(new CompileProvider(List("java", "scala").asJava))
+ capabilities.setRunProvider(new RunProvider(List("java", "scala").asJava))
+ capabilities.setTestProvider(new TestProvider(List("java", "scala").asJava))
+ capabilities.setDependencySourcesProvider(true)
+ capabilities.setInverseSourcesProvider(true)
+ capabilities.setResourcesProvider(true)
+ capabilities.setBuildTargetChangedProvider(false) //TODO: for now it's false, but will try to support this later
+ val future = new CompletableFuture[InitializeBuildResult]()
+ future.complete(new InitializeBuildResult("mill-bsp", millServerVersion, bspVersion, capabilities))
+ initialized = true
+ future
+ }
+
+ override def onBuildInitialized(): Unit = {
+ clientInitialized = true
+ }
+
+ override def buildShutdown(): CompletableFuture[Object] = {
+ handleExceptions[String, Object](_ => "shut down this server".asInstanceOf[Object], "")
+ }
+
+ override def onBuildExit(): Unit = {
+ cancelator()
+ }
+
+ override def workspaceBuildTargets(): CompletableFuture[WorkspaceBuildTargetsResult] = {
+ recomputeTargets()
+ handleExceptions[String, WorkspaceBuildTargetsResult](
+ _ => new WorkspaceBuildTargetsResult(moduleToTarget.values.toList.asJava),
+ "")
+ }
+
+ override def buildTargetSources(sourcesParams: SourcesParams): CompletableFuture[SourcesResult] = {
+ recomputeTargets()
+
+ def computeSourcesResult: SourcesResult = {
+ var items = List[SourcesItem]()
+
+ for (targetId <- sourcesParams.getTargets.asScala) {
+ var itemSources = List[SourceItem]()
+
+ val sources = evaluateInformativeTask(evaluator, targetIdToModule(targetId).sources, Agg.empty[PathRef]).
+ map(pathRef => pathRef.path).toSeq
+ val generatedSources = evaluateInformativeTask(evaluator,
+ targetIdToModule(targetId).generatedSources,
+ Agg.empty[PathRef]).
+ map(pathRef => pathRef.path).toSeq
+
+ for (source <- sources) {
+ itemSources ++= List(
+ new SourceItem(source.toIO.toURI.toString, SourceItemKind.DIRECTORY, false))
+ }
+
+ for (genSource <- generatedSources) {
+ itemSources ++= List(
+ new SourceItem(genSource.toIO.toURI.toString, SourceItemKind.DIRECTORY, true))
+ }
+
+ items ++= List(new SourcesItem(targetId, itemSources.asJava))
+ }
+
+ new SourcesResult(items.asJava)
+ }
+
+ handleExceptions[String, SourcesResult](_ => computeSourcesResult, "")
+ }
+
+ override def buildTargetInverseSources(inverseSourcesParams: InverseSourcesParams):
+ CompletableFuture[InverseSourcesResult] = {
+ recomputeTargets()
+
+ def getInverseSourcesResult: InverseSourcesResult = {
+ val textDocument = inverseSourcesParams.getTextDocument
+ val targets = millModules.filter(m => ModuleUtils.evaluateInformativeTask(
+ millEvaluator, m.allSourceFiles, Seq.empty[PathRef]).
+ map(pathRef => pathRef.path.toIO.toURI.toString).
+ contains(textDocument.getUri)).
+ map(m => moduleToTargetId(m))
+ new InverseSourcesResult(targets.asJava)
+ }
+
+ handleExceptions[String, InverseSourcesResult](_ => getInverseSourcesResult, "")
+ }
+
+ override def buildTargetDependencySources(dependencySourcesParams: DependencySourcesParams):
+ CompletableFuture[DependencySourcesResult] = {
+ recomputeTargets()
+
+ def getDependencySources: DependencySourcesResult = {
+ var items = List[DependencySourcesItem]()
+
+ for (targetId <- dependencySourcesParams.getTargets.asScala) {
+ val millModule = targetIdToModule(targetId)
+ var sources = evaluateInformativeTask(evaluator,
+ millModule.resolveDeps(millModule.transitiveIvyDeps),
+ Agg.empty[PathRef]) ++
+ evaluateInformativeTask(evaluator,
+ millModule.resolveDeps(millModule.compileIvyDeps),
+ Agg.empty[PathRef]) ++
+ evaluateInformativeTask(evaluator,
+ millModule.unmanagedClasspath,
+ Agg.empty[PathRef])
+ millModule match {
+ case _: ScalaModule => sources ++= evaluateInformativeTask(evaluator,
+ millModule.resolveDeps(millModule.asInstanceOf[ScalaModule].scalaLibraryIvyDeps),
+ Agg.empty[PathRef])
+ case _: JavaModule => sources ++= List()
+ }
+ items ++= List(new DependencySourcesItem(targetId, sources.
+ map(pathRef => pathRef.path.toIO.toURI.toString).
+ toList.asJava))
+ }
+
+ new DependencySourcesResult(items.asJava)
+ }
+
+ handleExceptions[String, DependencySourcesResult](_ => getDependencySources, "")
+ }
+
+ // Recompute the modules in the project in case any changes to the build took place
+ // and update all the mappings that depend on this info
+ private[this] def recomputeTargets(): Unit = {
+ rootModule = ModuleUtils.getRootJavaModule(millEvaluator.rootModule)
+ millModules = getMillModules(millEvaluator)
+ moduleToTargetId = ModuleUtils.getModuleTargetIdMap(millModules, millEvaluator)
+ targetIdToModule = targetToModule(moduleToTargetId)
+ moduleToTarget = ModuleUtils.millModulesToBspTargets(millModules, rootModule, evaluator, List("scala", "java"))
+ }
+
+ // Given the mapping from modules to targetIds, construct the mapping from targetIds to modules
+ private[this] def targetToModule(moduleToTargetId: Predef.Map[JavaModule, BuildTargetIdentifier]):
+ Predef.Map[BuildTargetIdentifier, JavaModule] = {
+ moduleToTargetId.keys.map(mod => (moduleToTargetId(mod), mod)).toMap
+
+ }
+
+ // Resolve all the mill modules contained in the project
+ private[this] def getMillModules(ev: Evaluator): Seq[JavaModule] = {
+ ev.rootModule.millInternal.segmentsToModules.values.
+ collect {
+ case m: scalalib.JavaModule => m
+ }.toSeq ++ Seq(rootModule)
+ }
+
+ // Given a function that take input of type T and return output of type V,
+ // apply the function on the given inputs and return a completable future of
+ // the result. If the execution of the function raises an Exception, complete
+ // the future exceptionally. Also complete exceptionally if the server was not
+ // yet initialized.
+ private[this] def handleExceptions[T, V](serverMethod: T => V, input: T): CompletableFuture[V] = {
+ val future = new CompletableFuture[V]()
+ if (initialized) {
+ try {
+ future.complete(serverMethod(input))
+ } catch {
+ case e: Exception => future.completeExceptionally(e)
+ }
+ } else {
+ future.completeExceptionally(
+ new Exception("Can not respond to any request before receiving the `initialize` request.")
+ )
+ }
+ future
+ }
+
+ override def buildTargetResources(resourcesParams: ResourcesParams): CompletableFuture[ResourcesResult] = {
+ recomputeTargets()
+
+ def getResources: ResourcesResult = {
+ var items = List[ResourcesItem]()
+
+ for (targetId <- resourcesParams.getTargets.asScala) {
+ val millModule = targetIdToModule(targetId)
+ val resources = evaluateInformativeTask(evaluator, millModule.resources, Agg.empty[PathRef]).
+ filter(pathRef => os.exists(pathRef.path)).
+ flatMap(pathRef => os.walk(pathRef.path)).
+ map(path => path.toIO.toURI.toString).
+ toList.asJava
+ items ++= List(new ResourcesItem(targetId, resources))
+ }
+ new ResourcesResult(items.asJava)
+ }
+
+ handleExceptions[String, ResourcesResult](_ => getResources, "")
+ }
+
+ //TODO: if the client wants to give compilation arguments and the module
+ // already has some from the build file, what to do?
+ override def buildTargetCompile(compileParams: CompileParams): CompletableFuture[CompileResult] = {
+ recomputeTargets()
+
+ def getCompileResult: CompileResult = {
+ val params = TaskParameters.fromCompileParams(compileParams)
+ val taskId = params.hashCode()
+ val compileTasks = Strict.Agg(params.getTargets.
+ filter(targetId => targetId != moduleToTarget(rootModule).getId).
+ map(targetId => targetIdToModule(targetId).compile): _*)
+ val result = millEvaluator.evaluate(compileTasks,
+ getBspLoggedReporterPool(params, t => s"Started compiling target: $t",
+ TaskDataKind.COMPILE_TASK, (targetId: BuildTargetIdentifier) => new CompileTask(targetId)),
+ DummyTestReporter,
+ new MillBspLogger(client, taskId, millEvaluator.log)
+ )
+ val compileResult = new CompileResult(getStatusCode(result))
+ compileResult.setOriginId(compileParams.getOriginId)
+ compileResult //TODO: See in what form IntelliJ expects data about products of compilation in order to set data field
+ }
+
+ handleExceptions[String, CompileResult](_ => getCompileResult, "")
+ }
+
+ override def buildTargetRun(runParams: RunParams): CompletableFuture[RunResult] = {
+ recomputeTargets()
+
+ def getRunResult: RunResult = {
+ val params = TaskParameters.fromRunParams(runParams)
+ val module = targetIdToModule(params.getTargets.head)
+ val args = params.getArguments.getOrElse(Seq.empty[String])
+ val runTask = module.run(args: _*)
+ val runResult = millEvaluator.evaluate(Strict.Agg(runTask),
+ getBspLoggedReporterPool(
+ params,
+ t => s"Started compiling target: $t",
+ TaskDataKind.COMPILE_TASK,
+ (targetId: BuildTargetIdentifier) => new CompileTask(targetId)),
+ logger = new MillBspLogger(client, runTask.hashCode(), millEvaluator.log))
+ val response = runResult.results(runTask) match {
+ case _: Result.Success[Any] => new RunResult(StatusCode.OK)
+ case _ => new RunResult(StatusCode.ERROR)
+ }
+ params.getOriginId match {
+ case Some(id) => response.setOriginId(id)
+ case None =>
+ }
+ response
+ }
+
+ handleExceptions[String, RunResult](_ => getRunResult, "")
+ }
+
+ override def buildTargetTest(testParams: TestParams): CompletableFuture[TestResult] = {
+ recomputeTargets()
+
+ def getTestResult: TestResult = {
+ val params = TaskParameters.fromTestParams(testParams)
+ val argsMap = try {
+ val scalaTestParams = testParams.getData.asInstanceOf[JsonObject]
+ (for (testItem <- scalaTestParams.get("testClasses").getAsJsonArray.asScala)
+ yield (
+ testItem.getAsJsonObject.get("target").getAsJsonObject.get("uri").getAsString,
+ testItem.getAsJsonObject.get("classes").getAsJsonArray
+ .asScala.map(elem => elem.getAsString).toSeq)).toMap
+ } catch {
+ case _: Exception => (for (targetId <- testParams.getTargets.asScala) yield
+ (targetId.getUri, Seq.empty[String])).toMap
+
+ }
+
+ var overallStatusCode = StatusCode.OK
+ for (targetId <- testParams.getTargets.asScala) {
+ val module = targetIdToModule(targetId)
+ module match {
+ case m: TestModule => val testModule = m.asInstanceOf[TestModule]
+ val testTask = testModule.testLocal(argsMap(targetId.getUri): _*)
+
+ // notifying the client that the testing of this build target started
+ val taskStartParams = new TaskStartParams(new TaskId(testTask.hashCode().toString))
+ taskStartParams.setEventTime(System.currentTimeMillis())
+ taskStartParams.setMessage("Testing target: " + targetId)
+ taskStartParams.setDataKind(TaskDataKind.TEST_TASK)
+ taskStartParams.setData(new TestTask(targetId))
+ client.onBuildTaskStart(taskStartParams)
+
+ val testReporter = new BspTestReporter(
+ client, targetId,
+ new TaskId(testTask.hashCode().toString),
+ Seq.empty[String])
+
+ val results = millEvaluator.evaluate(
+ Strict.Agg(testTask),
+ getBspLoggedReporterPool(params, t => s"Started compiling target: $t",
+ TaskDataKind.COMPILE_TASK, (targetId: BuildTargetIdentifier) => new CompileTask(targetId)),
+ testReporter,
+ new MillBspLogger(client, testTask.hashCode, millEvaluator.log))
+ val endTime = System.currentTimeMillis()
+ val statusCode = getStatusCode(results)
+ statusCode match {
+ case StatusCode.ERROR => overallStatusCode = StatusCode.ERROR
+ case StatusCode.CANCELLED => overallStatusCode =
+ if (overallStatusCode == StatusCode.ERROR) StatusCode.ERROR else StatusCode.CANCELLED
+ case StatusCode.OK =>
+ }
+ // notifying the client that the testing of this build target ended
+ val taskFinishParams = new TaskFinishParams(
+ new TaskId(testTask.hashCode().toString),
+ statusCode
+ )
+ taskFinishParams.setEventTime(endTime)
+ taskFinishParams.setMessage("Finished testing target: " +
+ moduleToTarget(targetIdToModule(targetId)).getDisplayName)
+ taskFinishParams.setDataKind(TaskDataKind.TEST_REPORT)
+ taskFinishParams.setData(testReporter.getTestReport)
+ client.onBuildTaskFinish(taskFinishParams)
+
+ case _ =>
+ }
+ }
+ val testResult = new TestResult(overallStatusCode)
+ params.getOriginId match {
+ case None => testResult
+ case Some(id) =>
+ //TODO: Add the messages from mill to the data field?
+ testResult.setOriginId(id)
+ testResult
+ }
+ }
+
+ handleExceptions[String, TestResult](_ => getTestResult, "")
+ }
+
+ // define the function that spawns compilation reporter for each module based on the
+ // module's hash code TODO: find something more reliable than the hash code
+ private[this] def getBspLoggedReporterPool(params: Parameters,
+ taskStartMessage: String => String,
+ taskStartDataKind: String,
+ taskStartData: BuildTargetIdentifier => Object):
+ Int => Option[BuildProblemReporter] = {
+ int: Int =>
+ if (moduleCodeToTargetId.contains(int)) {
+ val targetId = moduleCodeToTargetId(int)
+ val taskId = new TaskId(targetIdToModule(targetId).compile.hashCode.toString)
+ val taskStartParams = new TaskStartParams(taskId)
+ taskStartParams.setEventTime(System.currentTimeMillis())
+ taskStartParams.setData(taskStartData(targetId))
+ taskStartParams.setDataKind(taskStartDataKind)
+ taskStartParams.setMessage(taskStartMessage(moduleToTarget(targetIdToModule(targetId)).getDisplayName))
+ client.onBuildTaskStart(taskStartParams)
+ Option(new BspLoggedReporter(client,
+ targetId,
+ taskId,
+ params.getOriginId))
+ }
+ else None
+ }
+
+ // Get the execution status code given the results from Evaluator.evaluate
+ private[this] def getStatusCode(results: Evaluator.Results): StatusCode = {
+
+ val statusCodes = results.results.keys.map(task => getStatusCodePerTask(results, task)).toSeq
+ if (statusCodes.contains(StatusCode.ERROR))
+ StatusCode.ERROR
+ else if (statusCodes.contains(StatusCode.CANCELLED))
+ StatusCode.CANCELLED
+ else
+ StatusCode.OK
+ }
+
+ private[this] def getStatusCodePerTask(results: Evaluator.Results, task: mill.define.Task[_]): StatusCode = {
+ results.results(task) match {
+ case _: Success[_] => StatusCode.OK
+ case Skipped => StatusCode.CANCELLED
+ case _ => StatusCode.ERROR
+ }
+ }
+
+ override def buildTargetCleanCache(cleanCacheParams: CleanCacheParams): CompletableFuture[CleanCacheResult] = {
+ recomputeTargets()
+
+ def getCleanCacheResult: CleanCacheResult = {
+ var msg = ""
+ var cleaned = true
+ for (targetId <- cleanCacheParams.getTargets.asScala) {
+ val module = targetIdToModule(targetId)
+ val mainModule = new MainModule {
+ override implicit def millDiscover: Discover[_] = {
+ Discover[this.type]
+ }
+ }
+ val cleanTask = mainModule.clean(millEvaluator, List(s"${module.millModuleSegments.render}.compile"): _*)
+ val cleanResult = millEvaluator.evaluate(
+ Strict.Agg(cleanTask),
+ logger = new MillBspLogger(client, cleanTask.hashCode, millEvaluator.log)
+ )
+ if (cleanResult.failing.keyCount > 0) {
+ cleaned = false
+ msg += s" Target ${module.millModuleSegments.render} could not be cleaned. See message from mill: \n"
+ cleanResult.results(cleanTask) match {
+ case fail: Result.Failure[Any] => msg += fail.msg + "\n"
+ case _ => msg += "could not retrieve message"
+ }
+ } else {
+ msg += s"${module.millModuleSegments.render} cleaned \n"
+
+ val outDir = Evaluator.resolveDestPaths(os.pwd / "out", module.millModuleSegments ++
+ Seq(Label("compile"))).out
+ while (os.exists(outDir)) {
+ Thread.sleep(10)
+ }
+ }
+ }
+ new CleanCacheResult(msg, cleaned)
+ }
+
+ handleExceptions[String, CleanCacheResult](_ => getCleanCacheResult, "")
+ }
+
+ override def buildTargetScalacOptions(scalacOptionsParams: ScalacOptionsParams):
+ CompletableFuture[ScalacOptionsResult] = {
+ recomputeTargets()
+
+ def getScalacOptionsResult: ScalacOptionsResult = {
+ var targetScalacOptions = List.empty[ScalacOptionsItem]
+ for (targetId <- scalacOptionsParams.getTargets.asScala) {
+ val module = targetIdToModule(targetId)
+ module match {
+ case m: ScalaModule =>
+ val options = evaluateInformativeTask(evaluator, m.scalacOptions, Seq.empty[String]).toList
+ val classpath = evaluateInformativeTask(evaluator, m.runClasspath, Agg.empty[PathRef]).
+ map(pathRef => pathRef.path.toIO.toURI.toString).toList
+ val classDirectory = (Evaluator.resolveDestPaths(
+ os.pwd / "out",
+ m.millModuleSegments ++ Seq(Label("compile"))).dest / "classes"
+ ).toIO.toURI.toString
+
+ targetScalacOptions ++= List(new ScalacOptionsItem(targetId, options.asJava, classpath.asJava, classDirectory))
+ case _: JavaModule => targetScalacOptions ++= List()
+ }
+ }
+ new ScalacOptionsResult(targetScalacOptions.asJava)
+ }
+
+ handleExceptions[String, ScalacOptionsResult](_ => getScalacOptionsResult, "")
+ }
+
+ //TODO: In the case when mill fails to provide a main classes because multiple were
+ // defined for the same module, do something so that those can still be detected
+ // such that IntelliJ can run any of them
+ override def buildTargetScalaMainClasses(scalaMainClassesParams: ScalaMainClassesParams):
+ CompletableFuture[ScalaMainClassesResult] = {
+ recomputeTargets()
+
+ def getScalaMainClasses: ScalaMainClassesResult = {
+ var items = List.empty[ScalaMainClassesItem]
+ for (targetId <- scalaMainClassesParams.getTargets.asScala) {
+ val module = targetIdToModule(targetId)
+ val scalaMainClasses = getTaskResult(millEvaluator, module.finalMainClassOpt) match {
+ case result: Result.Success[Any] => result.asSuccess.get.value match {
+ case mainClass: Right[String, String] =>
+ List(new ScalaMainClass(
+ mainClass.value,
+ List.empty[String].asJava,
+ evaluateInformativeTask(evaluator, module.forkArgs, Seq.empty[String]).
+ toList.asJava))
+ case msg: Left[String, String] =>
+ val messageParams = new ShowMessageParams(MessageType.WARNING, msg.value)
+ messageParams.setOriginId(scalaMainClassesParams.getOriginId)
+ client.onBuildShowMessage(messageParams) // tell the client that no main class was found or specified
+ List.empty[ScalaMainClass]
+ }
+ case _ => List.empty[ScalaMainClass]
+ }
+ val item = new ScalaMainClassesItem(targetId, scalaMainClasses.asJava)
+ items ++= List(item)
+ }
+ new ScalaMainClassesResult(items.asJava)
+ }
+
+ handleExceptions[String, ScalaMainClassesResult](_ => getScalaMainClasses, "")
+ }
+
+ override def buildTargetScalaTestClasses(scalaTestClassesParams: ScalaTestClassesParams):
+ CompletableFuture[ScalaTestClassesResult] = {
+ recomputeTargets()
+
+ def getScalaTestClasses(implicit ctx: Ctx.Home): ScalaTestClassesResult = {
+ var items = List.empty[ScalaTestClassesItem]
+ for (targetId <- scalaTestClassesParams.getTargets.asScala) {
+ targetIdToModule(targetId) match {
+ case module: TestModule =>
+ items ++= List(new ScalaTestClassesItem(targetId, getTestClasses(module).toList.asJava))
+ case _: JavaModule => //TODO: maybe send a notification that this target has no test classes
+ }
+ }
+ new ScalaTestClassesResult(items.asJava)
+ }
+
+ handleExceptions[Ctx.Home, ScalaTestClassesResult](c => getScalaTestClasses(c), ctx)
+ }
+
+ // Detect and return the test classes contained in the given TestModule
+ private[this] def getTestClasses(module: TestModule)(implicit ctx: Ctx.Home): Seq[String] = {
+ val runClasspath = getTaskResult(millEvaluator, module.runClasspath)
+ val frameworks = getTaskResult(millEvaluator, module.testFrameworks)
+ val compilationResult = getTaskResult(millEvaluator, module.compile)
+
+ (runClasspath, frameworks, compilationResult) match {
+ case (Result.Success(classpath), Result.Success(testFrameworks), Result.Success(compResult)) =>
+ val classFingerprint = Jvm.inprocess(classpath.asInstanceOf[Seq[PathRef]].map(_.path),
+ classLoaderOverrideSbtTesting = true,
+ isolated = true,
+ closeContextClassLoaderWhenDone = false, cl => {
+ val fs = TestRunner.frameworks(testFrameworks.asInstanceOf[Seq[String]])(cl)
+ fs.flatMap(framework =>
+ discoverTests(cl, framework, Agg(compResult.asInstanceOf[CompilationResult].
+ classes.path)))
+ })
+ classFingerprint.map(classF => classF._1.getName.stripSuffix("$"))
+ case _ => Seq.empty[String] //TODO: or send notification that something went wrong
+ }
+ }
+
+}
diff --git a/contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala b/contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala
new file mode 100644
index 00000000..1fd0f019
--- /dev/null
+++ b/contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala
@@ -0,0 +1,297 @@
+package mill.contrib.bsp
+
+import ch.epfl.scala.bsp4j._
+import mill.T
+import mill.api.Result.Success
+import mill.api.{Loose, Strict}
+import mill.define._
+import mill.eval.{Evaluator, _}
+import mill.scalajslib.ScalaJSModule
+import mill.scalalib.api.Util
+import mill.scalalib.{JavaModule, ScalaModule, TestModule}
+import mill.scalanativelib._
+import os.Path
+
+import scala.collection.JavaConverters._
+
+/**
+ * Utilities for translating the mill build into
+ * BSP information like BuildTargets and BuildTargetIdentifiers
+ */
+object ModuleUtils {
+
+ /**
+ * Compute mapping between all the JavaModules contained in the
+ * working directory ( has to be a mill-based project ) and
+ * BSP BuildTargets ( mill modules correspond one-to-one to
+ * bsp build targets ).
+ *
+ * @param modules All JavaModules contained in the working
+ * directory of the mill project
+ * @param rootModule The root module ( corresponding to the root
+ * of the mill project )
+ * @param evaluator The mill evaluator that can resolve information
+ * about the mill project
+ * @param supportedLanguages the languages supported by the modules
+ * of the mill project
+ * @return JavaModule -> BuildTarget mapping
+ */
+ def millModulesToBspTargets(modules: Seq[JavaModule],
+ rootModule: JavaModule,
+ evaluator: Evaluator,
+ supportedLanguages: List[String]): Predef.Map[JavaModule, BuildTarget] = {
+
+ val moduleIdMap = getModuleTargetIdMap(modules, evaluator)
+
+ (for (module <- modules)
+ yield (module, getTarget(rootModule, module, evaluator, moduleIdMap))).toMap
+
+ }
+
+ /**
+ * Compute the BuildTarget associated with the given module,
+ * may or may not be identical to the root of the working
+ * directory ( rootModule )
+ *
+ * @param rootModule mill JavaModule for the project root
+ * @param module mill JavaModule to compute the BuildTarget
+ * for
+ * @param evaluator mill Evaluator
+ * @param moduleIdMap mapping from each mill JavaModule
+ * contained in the working directory and
+ * a BuildTargetIdentifier associated
+ * with it.
+ * @return build target for `module`
+ */
+ def getTarget(rootModule: JavaModule,
+ module: JavaModule,
+ evaluator: Evaluator,
+ moduleIdMap: Map[JavaModule, BuildTargetIdentifier]
+ ): BuildTarget = {
+ if (module == rootModule)
+ getRootTarget(module, evaluator, moduleIdMap)
+ else
+ getRegularTarget(module, evaluator, moduleIdMap)
+ }
+
+ /**
+ * Given the BaseModule corresponding to the root
+ * of the working directory, compute a JavaModule that
+ * has the same millSourcePath. Set generated sources
+ * according to the location of the compilation
+ * products
+ *
+ * @param rootBaseModule module for the root
+ * @return root JavaModule
+ */
+ def getRootJavaModule(rootBaseModule: BaseModule): JavaModule = {
+ implicit val ctx: Ctx = rootBaseModule.millOuterCtx
+ new JavaModule {
+
+ override def millSourcePath: Path = rootBaseModule.millSourcePath
+
+ override def sources = T.sources {
+ millSourcePath / "src"
+ }
+
+ def out = T.sources {
+ millSourcePath / "out"
+ }
+
+ def target = T.sources {
+ millSourcePath / "target"
+ }
+
+ override def generatedSources: Target[Seq[PathRef]] = T.sources {
+ out() ++ target()
+ }
+ }
+ }
+
+ /**
+ * Compute the BuildTarget associated with the root
+ * directory of the mill project being built
+ *
+ * @param rootModule the root JavaModule extracted from
+ * the build file by a mill evalautor
+ * @param evaluator mill evaluator that can resolve
+ * build information
+ * @param moduleIdMap mapping from each mill JavaModule
+ * contained in the working directory and
+ * a BuildTargetIdentifier associated
+ * with it.
+ * @return root BuildTarget
+ */
+ def getRootTarget(
+ rootModule: JavaModule,
+ evaluator: Evaluator,
+ moduleIdMap: Map[JavaModule, BuildTargetIdentifier]): BuildTarget = {
+
+ val rootTarget = new BuildTarget(
+ moduleIdMap(rootModule),
+ List.empty[String].asJava,
+ List.empty[String].asJava,
+ List.empty[BuildTargetIdentifier].asJava,
+ new BuildTargetCapabilities(false, false, false))
+ rootTarget.setBaseDirectory(rootModule.millSourcePath.toIO.toURI.toString)
+ rootTarget.setDataKind(BuildTargetDataKind.SCALA)
+ rootTarget.setTags(List(BuildTargetTag.LIBRARY, BuildTargetTag.APPLICATION).asJava)
+ rootTarget.setData(computeBuildTargetData(rootModule, evaluator))
+ val basePath = rootModule.millSourcePath.toIO.toPath
+ if (basePath.getNameCount >= 1)
+ rootTarget.setDisplayName(basePath.getName(basePath.getNameCount - 1) + "-root")
+ else rootTarget.setDisplayName("root")
+ rootTarget
+ }
+
+ /**
+ * Compute the BuildTarget associated with the given mill
+ * JavaModule, which is any module present in the working
+ * directory, but it's not the root module itself.
+ *
+ * @param module any in-project mill module
+ * @param evaluator mill evaluator
+ * @param moduleIdMap mapping from each mill JavaModule
+ * contained in the working directory and
+ * a BuildTargetIdentifier associated
+ * with it.
+ * @return inner BuildTarget
+ */
+ def getRegularTarget(
+ module: JavaModule,
+ evaluator: Evaluator,
+ moduleIdMap: Map[JavaModule, BuildTargetIdentifier]): BuildTarget = {
+ val dataBuildTarget = computeBuildTargetData(module, evaluator)
+ val capabilities = getModuleCapabilities(module, evaluator)
+ val buildTargetTag: List[String] = module match {
+ case m: TestModule => List(BuildTargetTag.TEST)
+ case m: JavaModule => List(BuildTargetTag.LIBRARY, BuildTargetTag.APPLICATION)
+ }
+
+ val dependencies = module match {
+ case m: JavaModule => m.moduleDeps.map(dep => moduleIdMap(dep)).toList.asJava
+ }
+
+ val buildTarget = new BuildTarget(moduleIdMap(module),
+ buildTargetTag.asJava,
+ List("scala", "java").asJava,
+ dependencies,
+ capabilities)
+ if (module.isInstanceOf[ScalaModule]) {
+ buildTarget.setDataKind(BuildTargetDataKind.SCALA)
+ }
+ buildTarget.setData(dataBuildTarget)
+ buildTarget.setDisplayName(moduleName(module.millModuleSegments))
+ buildTarget.setBaseDirectory(module.intellijModulePath.toIO.toURI.toString)
+ buildTarget
+ }
+
+ /**
+ * Evaluate the given task using the given mill evaluator and return
+ * its result of type Result
+ *
+ * @param evaluator mill evalautor
+ * @param task task to evaluate
+ * @tparam T
+ */
+ def getTaskResult[T](evaluator: Evaluator, task: Task[T]): Result[Any] = {
+ evaluator.evaluate(Strict.Agg(task)).results(task)
+ }
+
+ /**
+ * Evaluate the given task using the given mill evaluator and return
+ * its result of type T, or the default value of the evaluation failed.
+ *
+ * @param evaluator mill evalautor
+ * @param task task to evaluate
+ * @param defaultValue default value to return in case of failure
+ * @tparam T
+ */
+ def evaluateInformativeTask[T](evaluator: Evaluator, task: Task[T], defaultValue: T): T = {
+ val evaluated = evaluator.evaluate(Strict.Agg(task)).results(task)
+ evaluated match {
+ case Success(value) => evaluated.asSuccess.get.value.asInstanceOf[T]
+ case default => defaultValue
+ }
+ }
+
+ /**
+ * Compute mapping between a mill JavaModule and the BuildTargetIdentifier
+ * associated with its corresponding bsp BuildTarget.
+ *
+ * @param modules mill modules inside the project ( including root )
+ * @param evaluator mill evalautor to resolve build information
+ * @return JavaModule -> BuildTargetIdentifier mapping
+ */
+ def getModuleTargetIdMap(modules: Seq[JavaModule], evaluator: Evaluator): Predef.Map[JavaModule, BuildTargetIdentifier] = {
+
+ (for (module <- modules)
+ yield (module, new BuildTargetIdentifier(
+ (module.millOuterCtx.millSourcePath / os.RelPath(moduleName(module.millModuleSegments))).
+ toIO.toURI.toString))).toMap
+ }
+
+ // this is taken from mill.scalalib GenIdeaImpl
+ def moduleName(p: Segments) = p.value.foldLeft(StringBuilder.newBuilder) {
+ case (sb, Segment.Label(s)) if sb.isEmpty => sb.append(s)
+ case (sb, Segment.Cross(s)) if sb.isEmpty => sb.append(s.mkString("-"))
+ case (sb, Segment.Label(s)) => sb.append(".").append(s)
+ case (sb, Segment.Cross(s)) => sb.append("-").append(s.mkString("-"))
+ }.mkString.toLowerCase()
+
+ // obtain the capabilities of the given module ( ex: canCompile, canRun, canTest )
+ private[this] def getModuleCapabilities(module: JavaModule, evaluator: Evaluator): BuildTargetCapabilities = {
+ val canTest = module match {
+ case _: TestModule => true
+ case default => false
+ }
+
+ new BuildTargetCapabilities(true, canTest, true)
+ }
+
+ // Compute the ScalaBuildTarget from information about the given JavaModule.
+ //TODO: Fix the data field for JavaModule when the bsp specification is updated
+ private[this] def computeBuildTargetData(module: JavaModule, evaluator: Evaluator): ScalaBuildTarget = {
+ module match {
+ case m: ScalaModule =>
+ val scalaVersion = evaluateInformativeTask(evaluator, m.scalaVersion, "")
+ new ScalaBuildTarget(
+ evaluateInformativeTask(evaluator, m.scalaOrganization, ""),
+ scalaVersion,
+ Util.scalaBinaryVersion(scalaVersion),
+ getScalaTargetPlatform(m),
+ computeScalaLangDependencies(m, evaluator).
+ map(pathRef => pathRef.path.toIO.toURI.toString).
+ toList.asJava)
+
+ case m: JavaModule =>
+ val scalaVersion = "2.12.8"
+ new ScalaBuildTarget(
+ "or.scala-lang",
+ "2.12.8",
+ "2.12",
+ ScalaPlatform.JVM,
+ List.empty[String].asJava)
+ }
+ }
+
+ // Compute all relevant scala dependencies of `module`, like scala-library, scala-compiler,
+ // and scala-reflect
+ private[this] def computeScalaLangDependencies(module: ScalaModule, evaluator: Evaluator): Loose.Agg[PathRef] = {
+ evaluateInformativeTask(evaluator, module.resolveDeps(module.scalaLibraryIvyDeps), Loose.Agg.empty[PathRef]) ++
+ evaluateInformativeTask(evaluator, module.scalacPluginClasspath, Loose.Agg.empty[PathRef]) ++
+ evaluateInformativeTask(evaluator, module.resolveDeps(module.ivyDeps), Loose.Agg.empty[PathRef]).
+ filter(pathRef => pathRef.path.toIO.toURI.toString.contains("scala-compiler") ||
+ pathRef.path.toIO.toURI.toString.contains("scala-reflect") ||
+ pathRef.path.toIO.toURI.toString.contains("scala-library"))
+ }
+
+ // Obtain the scala platform for `module`
+ private[this] def getScalaTargetPlatform(module: ScalaModule): ScalaPlatform = {
+ module match {
+ case m: ScalaNativeModule => ScalaPlatform.NATIVE
+ case m: ScalaJSModule => ScalaPlatform.JS
+ case m: ScalaModule => ScalaPlatform.JVM
+ }
+ }
+}
diff --git a/contrib/bsp/src/mill/contrib/bsp/TaskParameters.scala b/contrib/bsp/src/mill/contrib/bsp/TaskParameters.scala
new file mode 100644
index 00000000..a235c922
--- /dev/null
+++ b/contrib/bsp/src/mill/contrib/bsp/TaskParameters.scala
@@ -0,0 +1,127 @@
+package mill.contrib.bsp
+
+import ch.epfl.scala.bsp4j.{BuildTargetIdentifier, CompileParams, RunParams, TestParams}
+
+import scala.collection.JavaConverters._
+
+
+/**
+ * Common trait to represent BSP request parameters that
+ * have a specific form: include one or more targetIds,
+ * arguments for the execution of the task, and an optional
+ * origin id generated by the client.
+ */
+trait Parameters {
+ def getTargets: List[BuildTargetIdentifier]
+
+ def getArguments: Option[Seq[String]]
+
+ def getOriginId: Option[String]
+}
+
+case class CParams(compileParams: CompileParams) extends Parameters {
+
+ override def getTargets: List[BuildTargetIdentifier] = {
+ compileParams.getTargets.asScala.toList
+ }
+
+ override def getArguments: Option[Seq[String]] = {
+ try {
+ Option(compileParams.getArguments.asScala)
+ } catch {
+ case e: Exception => Option.empty[Seq[String]]
+ }
+ }
+
+ override def getOriginId: Option[String] = {
+ try {
+ Option(compileParams.getOriginId)
+ } catch {
+ case e: Exception => Option.empty[String]
+ }
+ }
+
+}
+
+case class RParams(runParams: RunParams) extends Parameters {
+
+ override def getTargets: List[BuildTargetIdentifier] = {
+ List(runParams.getTarget)
+ }
+
+ override def getArguments: Option[Seq[String]] = {
+ try {
+ Option(runParams.getArguments.asScala)
+ } catch {
+ case e: Exception => Option.empty[Seq[String]]
+ }
+ }
+
+ override def getOriginId: Option[String] = {
+ try {
+ Option(runParams.getOriginId)
+ } catch {
+ case e: Exception => Option.empty[String]
+ }
+ }
+
+}
+
+case class TParams(testParams: TestParams) extends Parameters {
+
+ override def getTargets: List[BuildTargetIdentifier] = {
+ testParams.getTargets.asScala.toList
+ }
+
+ override def getArguments: Option[Seq[String]] = {
+ try {
+ Option(testParams.getArguments.asScala)
+ } catch {
+ case e: Exception => Option.empty[Seq[String]]
+ }
+ }
+
+ override def getOriginId: Option[String] = {
+ try {
+ Option(testParams.getOriginId)
+ } catch {
+ case e: Exception => Option.empty[String]
+ }
+ }
+}
+
+object TaskParameters {
+
+ /**
+ * Convert parameters specific to the compile request
+ * to the common trait Parameters.
+ *
+ * @param compileParams compile request parameters
+ * @return general task parameters containing compilation info
+ */
+ def fromCompileParams(compileParams: CompileParams): Parameters = {
+ CParams(compileParams)
+ }
+
+ /**
+ * Convert parameters specific to the run request
+ * to the common trait Parameters.
+ *
+ * @param runParams run request parameters
+ * @return general task parameters containing running info
+ */
+ def fromRunParams(runParams: RunParams): Parameters = {
+ RParams(runParams)
+ }
+
+ /**
+ * Convert parameters specific to the test request
+ * to the common trait Parameters.
+ *
+ * @param testParams compile request parameters
+ * @return general task parameters containing testing info
+ */
+ def fromTestParams(testParams: TestParams): Parameters = {
+ TParams(testParams)
+ }
+} \ No newline at end of file
diff --git a/main/api/src/mill/api/BuildReporter.scala b/main/api/src/mill/api/BuildReporter.scala
new file mode 100644
index 00000000..2b360a45
--- /dev/null
+++ b/main/api/src/mill/api/BuildReporter.scala
@@ -0,0 +1,97 @@
+package mill.api
+
+import java.io.File
+
+import sbt.testing.Event
+
+/**
+ * Test reporter class that can be
+ * injected into the test task and
+ * report information upon the start
+ * and the finish of testing events
+ */
+trait TestReporter {
+ def logStart(event: Event): Unit
+
+ def logFinish(event: Event): Unit
+
+
+}
+
+/**
+ * Dummy Test Reporter that doesn't report
+ * anything for any testing event.
+ */
+object DummyTestReporter extends TestReporter {
+ override def logStart(event: Event): Unit = {
+
+ }
+ override def logFinish(event: Event): Unit = {
+
+ }
+}
+
+/**
+ * A listener trait for getting notified about
+ * build output like compiler warnings and errors
+ */
+trait BuildProblemReporter {
+ def logError(problem: Problem): Unit
+
+ def logWarning(problem: Problem): Unit
+
+ def logInfo(problem: Problem): Unit
+
+ def printSummary(): Unit
+}
+
+/**
+ * Contains general information about the build problem
+ */
+trait Problem {
+ def category: String
+
+ def severity: Severity
+
+ def message: String
+
+ def position: ProblemPosition
+}
+
+/**
+ * Indicates the exact location (source file, line, column) of the build problem
+ */
+trait ProblemPosition {
+ def line: Option[Int]
+
+ def lineContent: String
+
+ def offset: Option[Int]
+
+ def pointer: Option[Int]
+
+ def pointerSpace: Option[String]
+
+ def sourcePath: Option[String]
+
+ def sourceFile: Option[File]
+
+ def startOffset: Option[Int] = Option.empty
+
+ def endOffset: Option[Int] = Option.empty
+
+ def startLine: Option[Int] = Option.empty
+
+ def startColumn: Option[Int] = Option.empty
+
+ def endLine: Option[Int] = Option.empty
+
+ def endColumn: Option[Int] = Option.empty
+}
+
+sealed trait Severity
+case object Info extends Severity
+case object Error extends Severity
+case object Warn extends Severity
+
+
diff --git a/main/api/src/mill/api/Ctx.scala b/main/api/src/mill/api/Ctx.scala
index 69d01f7e..e86ad7f9 100644
--- a/main/api/src/mill/api/Ctx.scala
+++ b/main/api/src/mill/api/Ctx.scala
@@ -2,7 +2,6 @@ package mill.api
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.implicitConversions
-
import os.Path
/**
@@ -60,7 +59,9 @@ class Ctx(
dest0: () => os.Path,
val log: Logger,
val home: os.Path,
- val env: Map[String, String]
+ val env: Map[String, String],
+ val reporter: Int => Option[BuildProblemReporter],
+ val testReporter: TestReporter
)
extends Ctx.Dest
with Ctx.Log
diff --git a/main/core/src/eval/Evaluator.scala b/main/core/src/eval/Evaluator.scala
index f4ec8ff9..1b58660b 100644
--- a/main/core/src/eval/Evaluator.scala
+++ b/main/core/src/eval/Evaluator.scala
@@ -2,17 +2,18 @@ package mill.eval
import java.net.URLClassLoader
-import scala.collection.JavaConverters._
-import scala.collection.mutable
-import scala.util.control.NonFatal
-
import ammonite.runtime.SpecialClassLoader
-import mill.util.Router.EntryPoint
-import mill.define.{Ctx => _, _}
import mill.api.Result.{Aborted, OuterStack, Success}
+import mill.api.Strict.Agg
+import mill.api.{DummyTestReporter, TestReporter, BuildProblemReporter}
+import mill.define.{Ctx => _, _}
import mill.util
+import mill.util.Router.EntryPoint
import mill.util._
-import mill.api.Strict.Agg
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+import scala.util.control.NonFatal
case class Labelled[T](task: NamedTask[T],
segments: Segments){
@@ -40,9 +41,16 @@ case class Evaluator(home: os.Path,
val classLoaderSignHash = classLoaderSig.hashCode()
- def evaluate(goals: Agg[Task[_]]): Evaluator.Results = {
+ /**
+ * @param goals The tasks that need to be evaluated
+ * @param reporter A function that will accept a module id and provide a listener for build problems in that module
+ * @param testReporter Listener for test events like start, finish with success/error
+ */
+ def evaluate(goals: Agg[Task[_]],
+ reporter: Int => Option[BuildProblemReporter] = (int: Int) => Option.empty[BuildProblemReporter],
+ testReporter: TestReporter = DummyTestReporter,
+ logger: Logger = log): Evaluator.Results = {
os.makeDir.all(outPath)
-
val (sortedGroups, transitive) = Evaluator.plan(rootModule, goals)
val evaluated = new Agg.Mutable[Task[_]]
@@ -66,7 +74,10 @@ case class Evaluator(home: os.Path,
terminal,
group,
results,
- counterMsg
+ counterMsg,
+ reporter,
+ testReporter,
+ logger
)
someTaskFailed = someTaskFailed || newResults.exists(task => !task._2.isInstanceOf[Success[_]])
@@ -111,7 +122,10 @@ case class Evaluator(home: os.Path,
def evaluateGroupCached(terminal: Either[Task[_], Labelled[_]],
group: Agg[Task[_]],
results: collection.Map[Task[_], Result[(Any, Int)]],
- counterMsg: String
+ counterMsg: String,
+ zincProblemReporter: Int => Option[BuildProblemReporter],
+ testReporter: TestReporter,
+ logger: Logger
): (collection.Map[Task[_], Result[(Any, Int)]], Seq[Task[_]], Boolean) = {
val externalInputsHash = scala.util.hashing.MurmurHash3.orderedHash(
@@ -133,7 +147,10 @@ case class Evaluator(home: os.Path,
inputsHash,
paths = None,
maybeTargetLabel = None,
- counterMsg = counterMsg
+ counterMsg = counterMsg,
+ zincProblemReporter,
+ testReporter,
+ logger
)
(newResults, newEvaluated, false)
case Right(labelledNamedTask) =>
@@ -185,7 +202,10 @@ case class Evaluator(home: os.Path,
inputsHash,
paths = Some(paths),
maybeTargetLabel = Some(msgParts.mkString),
- counterMsg = counterMsg
+ counterMsg = counterMsg,
+ zincProblemReporter,
+ testReporter,
+ logger
)
newResults(labelledNamedTask.task) match{
@@ -259,7 +279,10 @@ case class Evaluator(home: os.Path,
inputsHash: Int,
paths: Option[Evaluator.Paths],
maybeTargetLabel: Option[String],
- counterMsg: String): (mutable.LinkedHashMap[Task[_], Result[(Any, Int)]], mutable.Buffer[Task[_]]) = {
+ counterMsg: String,
+ reporter: Int => Option[BuildProblemReporter],
+ testReporter: TestReporter,
+ logger: Logger): (mutable.LinkedHashMap[Task[_], Result[(Any, Int)]], mutable.Buffer[Task[_]]) = {
val newEvaluated = mutable.Buffer.empty[Task[_]]
@@ -276,11 +299,11 @@ case class Evaluator(home: os.Path,
val logRun = inputResults.forall(_.isInstanceOf[Result.Success[_]])
val prefix = s"[$counterMsg] $targetLabel "
- if(logRun) log.ticker(prefix)
+ if(logRun) logger.ticker(prefix)
prefix + "| "
}
- val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log))) {
+ val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) {
override def ticker(s: String): Unit = {
super.ticker(tickerPrefix.getOrElse("")+s)
}
@@ -319,7 +342,9 @@ case class Evaluator(home: os.Path,
},
multiLogger,
home,
- env
+ env,
+ reporter,
+ testReporter
)
val out = System.out
@@ -359,9 +384,9 @@ case class Evaluator(home: os.Path,
(newResults, newEvaluated)
}
- def resolveLogger(logPath: Option[os.Path]): Logger = logPath match{
- case None => log
- case Some(path) => MultiLogger(log.colored, log, FileLogger(log.colored, path, debugEnabled = true))
+ def resolveLogger(logPath: Option[os.Path], logger: Logger): Logger = logPath match{
+ case None => logger
+ case Some(path) => MultiLogger(logger.colored, log, FileLogger(logger.colored, path, debugEnabled = true))
}
}
diff --git a/scalajslib/src/ScalaJSModule.scala b/scalajslib/src/ScalaJSModule.scala
index 7dba4e72..741ab29e 100644
--- a/scalajslib/src/ScalaJSModule.scala
+++ b/scalajslib/src/ScalaJSModule.scala
@@ -195,7 +195,8 @@ trait TestScalaJSModule extends ScalaJSModule with TestModule {
_ => Seq(framework),
runClasspath().map(_.path),
Agg(compile().classes.path),
- args
+ args,
+ T.ctx.testReporter
)
val res = TestModule.handleResults(doneMsg, results)
// Hack to try and let the Node.js subprocess finish streaming it's stdout
diff --git a/scalalib/api/src/ZincWorkerApi.scala b/scalalib/api/src/ZincWorkerApi.scala
index 53a98c24..dfda44b8 100644
--- a/scalalib/api/src/ZincWorkerApi.scala
+++ b/scalalib/api/src/ZincWorkerApi.scala
@@ -1,8 +1,11 @@
package mill.scalalib.api
import mill.api.Loose.Agg
-import mill.api.PathRef
+import mill.api.{PathRef, BuildProblemReporter}
import mill.api.JsonFormatters._
+
+
+
object ZincWorkerApi{
type Ctx = mill.api.Ctx.Dest with mill.api.Ctx.Log with mill.api.Ctx.Home
}
@@ -11,7 +14,8 @@ trait ZincWorkerApi {
def compileJava(upstreamCompileOutput: Seq[CompilationResult],
sources: Agg[os.Path],
compileClasspath: Agg[os.Path],
- javacOptions: Seq[String])
+ javacOptions: Seq[String],
+ reporter: Option[BuildProblemReporter])
(implicit ctx: ZincWorkerApi.Ctx): mill.api.Result[CompilationResult]
/** Compile a mixed Scala/Java or Scala-only project */
@@ -23,7 +27,8 @@ trait ZincWorkerApi {
scalaOrganization: String,
scalacOptions: Seq[String],
compilerClasspath: Agg[os.Path],
- scalacPluginClasspath: Agg[os.Path])
+ scalacPluginClasspath: Agg[os.Path],
+ reporter: Option[BuildProblemReporter])
(implicit ctx: ZincWorkerApi.Ctx): mill.api.Result[CompilationResult]
def discoverMainClasses(compilationResult: CompilationResult)
diff --git a/scalalib/src/JavaModule.scala b/scalalib/src/JavaModule.scala
index dc412f3b..54fd5943 100644
--- a/scalalib/src/JavaModule.scala
+++ b/scalalib/src/JavaModule.scala
@@ -208,15 +208,17 @@ trait JavaModule extends mill.Module with TaskModule with GenIdeaModule { outer
} yield PathRef(path)
}
+
/**
* Compiles the current module to generate compiled classfiles/bytecode
*/
- def compile: T[mill.scalalib.api.CompilationResult] = T.persistent{
+ def compile: T[mill.scalalib.api.CompilationResult] = T.persistent {
zincWorker.worker().compileJava(
upstreamCompileOutput(),
allSourceFiles().map(_.path),
compileClasspath().map(_.path),
- javacOptions()
+ javacOptions(),
+ T.ctx().reporter(hashCode)
)
}
@@ -625,7 +627,8 @@ trait TestModule extends JavaModule with TaskModule {
TestRunner.frameworks(testFrameworks()),
runClasspath().map(_.path),
Agg(compile().classes.path),
- args
+ args,
+ T.ctx().testReporter
)
TestModule.handleResults(doneMsg, results)
diff --git a/scalalib/src/ScalaModule.scala b/scalalib/src/ScalaModule.scala
index 0ebd5700..daf4adcc 100644
--- a/scalalib/src/ScalaModule.scala
+++ b/scalalib/src/ScalaModule.scala
@@ -128,7 +128,8 @@ trait ScalaModule extends JavaModule { outer =>
resolveDeps(T.task{runIvyDeps() ++ scalaLibraryIvyDeps() ++ transitiveIvyDeps()})()
}
- override def compile: T[mill.scalalib.api.CompilationResult] = T.persistent{
+ override def compile: T[mill.scalalib.api.CompilationResult] = T.persistent {
+
zincWorker.worker().compileMixed(
upstreamCompileOutput(),
allSourceFiles().map(_.path),
@@ -139,6 +140,7 @@ trait ScalaModule extends JavaModule { outer =>
scalacOptions(),
scalaCompilerClasspath().map(_.path),
scalacPluginClasspath().map(_.path),
+ T.ctx().reporter(hashCode)
)
}
diff --git a/scalalib/src/TestRunner.scala b/scalalib/src/TestRunner.scala
index 42e65d63..8d69723f 100644
--- a/scalalib/src/TestRunner.scala
+++ b/scalalib/src/TestRunner.scala
@@ -1,11 +1,13 @@
package mill.scalalib
import ammonite.util.Colors
import mill.Agg
+import mill.api.{DummyTestReporter, TestReporter}
import mill.modules.Jvm
import mill.scalalib.Lib.discoverTests
import mill.util.{Ctx, PrintLogger}
import mill.util.JsonFormatters._
import sbt.testing._
+import mill.scalalib.api._
import scala.collection.mutable
object TestRunner {
@@ -45,7 +47,8 @@ object TestRunner {
frameworkInstances = TestRunner.frameworks(frameworks),
entireClasspath = Agg.from(classpath.map(os.Path(_))),
testClassfilePath = Agg(os.Path(testCp)),
- args = arguments
+ args = arguments,
+ DummyTestReporter
)(ctx)
// Clear interrupted state in case some badly-behaved test suite
@@ -66,7 +69,8 @@ object TestRunner {
def runTests(frameworkInstances: ClassLoader => Seq[sbt.testing.Framework],
entireClasspath: Agg[os.Path],
testClassfilePath: Agg[os.Path],
- args: Seq[String])
+ args: Seq[String],
+ testReporter: TestReporter)
(implicit ctx: Ctx.Log with Ctx.Home): (String, Seq[mill.scalalib.TestRunner.Result]) = {
//Leave the context class loader set and open so that shutdown hooks can access it
Jvm.inprocess(entireClasspath, classLoaderOverrideSbtTesting = true, isolated = true, closeContextClassLoaderWhenDone = false, cl => {
@@ -88,7 +92,11 @@ object TestRunner {
while (taskQueue.nonEmpty){
val next = taskQueue.dequeue().execute(
new EventHandler {
- def handle(event: Event) = events.append(event)
+ def handle(event: Event) = {
+ testReporter.logStart(event)
+ events.append(event)
+ testReporter.logFinish(event)
+ }
},
Array(
new Logger {
@@ -113,19 +121,19 @@ object TestRunner {
val results = for(e <- events) yield {
val ex = if (e.throwable().isDefined) Some(e.throwable().get) else None
mill.scalalib.TestRunner.Result(
- e.fullyQualifiedName(),
- e.selector() match{
+ e.fullyQualifiedName(),
+ e.selector() match{
case s: NestedSuiteSelector => s.suiteId()
case s: NestedTestSelector => s.suiteId() + "." + s.testName()
case s: SuiteSelector => s.toString
case s: TestSelector => s.testName()
case s: TestWildcardSelector => s.testWildcard()
},
- e.duration(),
- e.status().toString,
- ex.map(_.getClass.getName),
- ex.map(_.getMessage),
- ex.map(_.getStackTrace)
+ e.duration(),
+ e.status().toString,
+ ex.map(_.getClass.getName),
+ ex.map(_.getMessage),
+ ex.map(_.getStackTrace)
)
}
diff --git a/scalalib/worker/src/ZincWorkerImpl.scala b/scalalib/worker/src/ZincWorkerImpl.scala
index cf37812c..0f2cbf10 100644
--- a/scalalib/worker/src/ZincWorkerImpl.scala
+++ b/scalalib/worker/src/ZincWorkerImpl.scala
@@ -3,16 +3,17 @@ package mill.scalalib.worker
import java.io.File
import java.util.Optional
-import scala.ref.WeakReference
-
import mill.api.Loose.Agg
-import mill.api.{KeyedLockedCache, PathRef}
-import xsbti.compile.{CompilerCache => _, FileAnalysisStore => _, ScalaInstance => _, _}
+import mill.api.{Info, KeyedLockedCache, PathRef, Problem, ProblemPosition, Severity, Warn, BuildProblemReporter}
import mill.scalalib.api.Util.{grepJar, isDotty, scalaBinaryVersion}
+import mill.scalalib.api.{CompilationResult, ZincWorkerApi}
import sbt.internal.inc._
import sbt.internal.util.{ConsoleOut, MainAppender}
import sbt.util.LogExchange
-import mill.scalalib.api.{CompilationResult, ZincWorkerApi}
+import xsbti.compile.{CompilerCache => _, FileAnalysisStore => _, ScalaInstance => _, _}
+
+import scala.ref.WeakReference
+
case class MockedLookup(am: File => Optional[CompileAnalysis]) extends PerClasspathEntryLookup {
override def analysis(classpathEntry: File): Optional[CompileAnalysis] =
am(classpathEntry)
@@ -21,6 +22,45 @@ case class MockedLookup(am: File => Optional[CompileAnalysis]) extends PerClassp
Locate.definesClass(classpathEntry)
}
+class ZincProblem(base: xsbti.Problem) extends Problem {
+ override def category: String = base.category()
+
+ override def severity: Severity = base.severity() match {
+ case xsbti.Severity.Info => mill.api.Info
+ case xsbti.Severity.Warn => mill.api.Warn
+ case xsbti.Severity.Error => mill.api.Error
+ }
+
+ override def message: String = base.message()
+
+ override def position: ProblemPosition = new ZincProblemPosition(base.position())
+}
+
+class ZincProblemPosition(base: xsbti.Position) extends ProblemPosition {
+
+ object JavaOptionConverter {
+ implicit def convertInt(x: Optional[Integer]): Option[Int] = if (x.isEmpty) None else Some(x.get().intValue())
+ implicit def convert[T](x: Optional[T]): Option[T] = if (x.isEmpty) None else Some(x.get())
+ }
+
+ import JavaOptionConverter._
+
+ override def line: Option[Int] = base.line()
+
+ override def lineContent: String = base.lineContent()
+
+ override def offset: Option[Int] = base.offset()
+
+ override def pointer: Option[Int] = base.pointer()
+
+ override def pointerSpace: Option[String] = base.pointerSpace()
+
+ override def sourcePath: Option[String] = base.sourcePath()
+
+ override def sourceFile: Option[File] = base.sourceFile()
+}
+
+
class ZincWorkerImpl(compilerBridge: Either[
(ZincWorkerApi.Ctx, (String, String) => (Option[Array[os.Path]], os.Path)),
String => os.Path
@@ -61,7 +101,7 @@ class ZincWorkerImpl(compilerBridge: Either[
scalaVersion,
scalaOrganization,
compilerClasspath,
- scalacPluginClasspath,
+ scalacPluginClasspath
) { compilers: Compilers =>
val scaladocClass = compilers.scalac().scalaInstance().loader().loadClass("scala.tools.nsc.ScalaDoc")
val scaladocMethod = scaladocClass.getMethod("process", classOf[Array[String]])
@@ -155,20 +195,23 @@ class ZincWorkerImpl(compilerBridge: Either[
def compileJava(upstreamCompileOutput: Seq[CompilationResult],
sources: Agg[os.Path],
compileClasspath: Agg[os.Path],
- javacOptions: Seq[String])
+ javacOptions: Seq[String],
+ reporter: Option[BuildProblemReporter])
(implicit ctx: ZincWorkerApi.Ctx): mill.api.Result[CompilationResult] = {
for(res <- compileJava0(
upstreamCompileOutput.map(c => (c.analysisFile, c.classes.path)),
sources,
compileClasspath,
- javacOptions
+ javacOptions,
+ reporter
)) yield CompilationResult(res._1, PathRef(res._2))
}
def compileJava0(upstreamCompileOutput: Seq[(os.Path, os.Path)],
sources: Agg[os.Path],
compileClasspath: Agg[os.Path],
- javacOptions: Seq[String])
+ javacOptions: Seq[String],
+ reporter: Option[BuildProblemReporter])
(implicit ctx: ZincWorkerApi.Ctx): mill.api.Result[(os.Path, os.Path)] = {
compileInternal(
upstreamCompileOutput,
@@ -176,7 +219,8 @@ class ZincWorkerImpl(compilerBridge: Either[
compileClasspath,
javacOptions,
scalacOptions = Nil,
- javaOnlyCompilers
+ javaOnlyCompilers,
+ reporter
)
}
@@ -188,7 +232,8 @@ class ZincWorkerImpl(compilerBridge: Either[
scalaOrganization: String,
scalacOptions: Seq[String],
compilerClasspath: Agg[os.Path],
- scalacPluginClasspath: Agg[os.Path])
+ scalacPluginClasspath: Agg[os.Path],
+ reporter: Option[BuildProblemReporter])
(implicit ctx: ZincWorkerApi.Ctx): mill.api.Result[CompilationResult] = {
for (res <- compileMixed0(
@@ -200,7 +245,8 @@ class ZincWorkerImpl(compilerBridge: Either[
scalaOrganization,
scalacOptions,
compilerClasspath,
- scalacPluginClasspath
+ scalacPluginClasspath,
+ reporter
)) yield CompilationResult(res._1, PathRef(res._2))
}
@@ -212,13 +258,14 @@ class ZincWorkerImpl(compilerBridge: Either[
scalaOrganization: String,
scalacOptions: Seq[String],
compilerClasspath: Agg[os.Path],
- scalacPluginClasspath: Agg[os.Path])
+ scalacPluginClasspath: Agg[os.Path],
+ reporter: Option[BuildProblemReporter])
(implicit ctx: ZincWorkerApi.Ctx): mill.api.Result[(os.Path, os.Path)] = {
withCompilers(
scalaVersion,
scalaOrganization,
compilerClasspath,
- scalacPluginClasspath,
+ scalacPluginClasspath
) {compilers: Compilers =>
compileInternal(
upstreamCompileOutput,
@@ -226,7 +273,8 @@ class ZincWorkerImpl(compilerBridge: Either[
compileClasspath,
javacOptions,
scalacOptions = scalacPluginClasspath.map(jar => s"-Xplugin:$jar").toSeq ++ scalacOptions,
- compilers
+ compilers,
+ reporter
)
}
}
@@ -298,7 +346,8 @@ class ZincWorkerImpl(compilerBridge: Either[
compileClasspath: Agg[os.Path],
javacOptions: Seq[String],
scalacOptions: Seq[String],
- compilers: Compilers)
+ compilers: Compilers,
+ reporter: Option[BuildProblemReporter])
(implicit ctx: ZincWorkerApi.Ctx): mill.api.Result[(os.Path, os.Path)] = {
os.makeDir.all(ctx.dest)
@@ -312,7 +361,25 @@ class ZincWorkerImpl(compilerBridge: Either[
LogExchange.bindLoggerAppenders(id, (consoleAppender -> sbt.util.Level.Info) :: Nil)
l
}
+ val newReporter = reporter match {
+ case None => new ManagedLoggedReporter(10, logger)
+ case Some(r) => new ManagedLoggedReporter(10, logger) {
+ override def logError(problem: xsbti.Problem): Unit = {
+ r.logError(new ZincProblem(problem))
+ super.logError(problem)
+ }
+
+ override def logWarning(problem: xsbti.Problem): Unit = {
+ r.logWarning(new ZincProblem(problem))
+ super.logWarning(problem)
+ }
+ override def logInfo(problem: xsbti.Problem): Unit = {
+ r.logInfo(new ZincProblem(problem))
+ super.logInfo(problem)
+ }
+ }
+ }
val analysisMap0 = upstreamCompileOutput.map(_.swap).toMap
def analysisMap(f: File): Optional[CompileAnalysis] = {
@@ -350,7 +417,7 @@ class ZincWorkerImpl(compilerBridge: Either[
zincIOFile,
new FreshCompilerCache,
IncOptions.of(),
- new ManagedLoggedReporter(10, logger),
+ newReporter,
None,
Array()
),
@@ -372,7 +439,6 @@ class ZincWorkerImpl(compilerBridge: Either[
newResult.setup()
)
)
-
mill.api.Result.Success((zincFile, classesDir))
}catch{case e: CompileFailed => mill.api.Result.Failure(e.toString)}
}
diff --git a/scalanativelib/src/ScalaNativeModule.scala b/scalanativelib/src/ScalaNativeModule.scala
index d6fb66bd..389d1b6a 100644
--- a/scalanativelib/src/ScalaNativeModule.scala
+++ b/scalanativelib/src/ScalaNativeModule.scala
@@ -188,7 +188,8 @@ trait TestScalaNativeModule extends ScalaNativeModule with TestModule { testOute
nativeFrameworks,
runClasspath().map(_.path),
Agg(compile().classes.path),
- args
+ args,
+ T.ctx.testReporter
)
TestModule.handleResults(doneMsg, results)