diff options
Diffstat (limited to 'contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala')
-rw-r--r-- | contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala | 297 |
1 files changed, 297 insertions, 0 deletions
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 + } + } +} |