summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTobias Roeser <le.petit.fou@web.de>2019-10-16 13:07:51 +0200
committerTobias Roeser <le.petit.fou@web.de>2019-10-16 13:07:51 +0200
commit6a19be4008426bb4edc137dd756d0a0b7347e0a5 (patch)
tree7add40a2be94061cac8767bb36e987a304077854
parent9354e9311118ca23a44f72e463875a2769e53716 (diff)
parent76a8dfe705cafc18836df122d45c51452d4c6de3 (diff)
downloadmill-6a19be4008426bb4edc137dd756d0a0b7347e0a5.tar.gz
mill-6a19be4008426bb4edc137dd756d0a0b7347e0a5.tar.bz2
mill-6a19be4008426bb4edc137dd756d0a0b7347e0a5.zip
Merge pull request https://github.com/lihaoyi/mill/pull/664
Added the contrib.bsp module which contains an implementation of the BuildServer from BSP, thus alowing mill to be used by IDEs which use BSP. The MillBuildServer supports the following BSP features: * retrieving the build targets * compile requests * run requests * test requests * compile published diagnostics * task start/finish notifications for compile and test requests * progress notifications for compile * retrieving the scala main classes, test classes and scalac options Currently these features allow importing and compiling a mill project in IntelliJ IDEA via BSP. Known issues, some of which are being investigated: * can not run main classes from the IntelliJ interface * rarely, a strange NoClassDefFoundException is being thrown upon compiling from intellij * still tweaking the command for starting the server in order to work on all operating systems ( should be fine for linux and macOs so far ) Would be great to get feedback about this integration.
-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)