summaryrefslogtreecommitdiff
path: root/contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala
blob: 8cccf7b3a560fc6275a5a16318e4335a6f233ff2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package mill.contrib.bsp

import scala.collection.JavaConverters._
import ch.epfl.scala.bsp4j._
import mill.T
import mill.api.Result.Success
import mill.api.{Loose, Strict}
import mill.define.{BaseModule, Ctx, Segment, Segments, Target, Task}
import mill.eval._
import mill.eval.Evaluator
import mill.scalajslib.ScalaJSModule
import mill.scalalib.api.Util
import mill.scalanativelib._
import mill.scalalib.{JavaModule, ScalaModule, TestModule}
import os.Path

/**
  * 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
  }

  // 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)
    }
  }

  /**
    * 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 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
    }
  }

  /**
    * 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()
}