diff options
20 files changed, 1775 insertions, 60 deletions
@@ -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 @@ -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) |