diff options
Diffstat (limited to 'contrib/bloop/src')
-rw-r--r-- | contrib/bloop/src/mill.contrib.bloop/BloopImpl.scala | 356 | ||||
-rw-r--r-- | contrib/bloop/src/mill.contrib.bloop/CirceCompat.scala | 23 | ||||
-rw-r--r-- | contrib/bloop/src/mill/contrib/Bloop.scala | 10 |
3 files changed, 389 insertions, 0 deletions
diff --git a/contrib/bloop/src/mill.contrib.bloop/BloopImpl.scala b/contrib/bloop/src/mill.contrib.bloop/BloopImpl.scala new file mode 100644 index 00000000..e99b25d5 --- /dev/null +++ b/contrib/bloop/src/mill.contrib.bloop/BloopImpl.scala @@ -0,0 +1,356 @@ +package mill.contrib.bloop + +import ammonite.ops._ +import bloop.config.ConfigEncoderDecoders._ +import bloop.config.{Config => BloopConfig} +import mill._ +import mill.api.Loose +import mill.define.{Module => MillModule, _} +import mill.eval.Evaluator +import mill.scalalib._ +import os.pwd + +/** + * Implementation of the Bloop related tasks. Inherited by the + * `mill.contrib.Bloop` object, and usable in tests by passing + * a custom evaluator. + */ +class BloopImpl(ev: () => Evaluator, wd: Path) extends ExternalModule { outer => + + /** + * Generates bloop configuration files reflecting the build, + * under pwd/.bloop. + */ + def install = T { + Task.traverse(computeModules)(_.bloop.writeConfig) + } + + /** + * Trait that can be mixed-in to quickly access the bloop config + * of the module. + * + * {{{ + * object myModule extends ScalaModule with Bloop.Module { + * ... + * } + * }}} + */ + trait Module extends MillModule with CirceCompat { self: JavaModule => + + object bloop extends MillModule { + def config = T { + new BloopOps(self).bloop.config() + } + } + } + + /** + * Extension class used to ensure that the config related tasks are + * cached alongside their respective modules, without requesting the user + * to extend a specific trait. + * + * This also ensures that we're not duplicating work between the global + * "install" task that traverse all modules in the build, and "local" tasks + * that traverse only their transitive dependencies. + */ + private implicit class BloopOps(jm: JavaModule) + extends MillModule + with CirceCompat { + override def millOuterCtx = jm.millOuterCtx + + object bloop extends MillModule { + def config = T { outer.bloopConfig(jm) } + + def writeConfig: Target[(String, PathRef)] = T { + mkdir(bloopDir) + val path = bloopConfigPath(jm) + _root_.bloop.config.write(config(), path.toNIO) + T.ctx().log.info(s"Wrote $path") + name(jm) -> PathRef(path) + } + + def writeTransitiveConfig = T { + Task.traverse(jm.transitiveModuleDeps)(_.bloop.writeConfig) + } + } + } + + private val bloopDir = wd / ".bloop" + + private def computeModules: Seq[JavaModule] = { + val eval = ev() + if (eval != null) { + val rootModule = eval.rootModule + rootModule.millInternal.segmentsToModules.values.collect { + case m: scalalib.JavaModule => m + }.toSeq + } else Seq() + } + + /** + * Computes sources files paths for the whole project. Cached in a way + * that does not get invalidated upon sourcefile change. Mainly called + * from module#sources in bloopInstall + */ + def moduleSourceMap: Target[Map[String, Seq[Path]]] = T { + val sources = Task.traverse(computeModules) { m => + m.allSources.map { paths => + m.millModuleSegments.render -> paths.map(_.path) + } + }() + sources.toMap + } + + protected def name(m: JavaModule) = m.millModuleSegments.render + + protected def bloopConfigPath(module: JavaModule): Path = + bloopDir / s"${name(module)}.json" + + ////////////////////////////////////////////////////////////////////////////// + // SemanticDB related configuration + ////////////////////////////////////////////////////////////////////////////// + + // Version of the semanticDB plugin. + def semanticDBVersion: String = "4.1.4" + + // Scala versions supported by semantic db. Needs to be updated when + // bumping semanticDBVersion. + // See [https://github.com/scalameta/metals/blob/333ab6fc00fb3542bcabd0dac51b91b72798768a/build.sbt#L121] + def semanticDBSupported = Set( + "2.12.8", + "2.12.7", + "2.12.6", + "2.12.5", + "2.12.4", + "2.11.12", + "2.11.11", + "2.11.10", + "2.11.9" + ) + + // Recommended for metals usage. + def semanticDBOptions = List( + s"-P:semanticdb:sourceroot:$pwd", + "-P:semanticdb:synthetics:on", + "-P:semanticdb:failures:warning" + ) + + ////////////////////////////////////////////////////////////////////////////// + // Computation of the bloop configuration for a specific module + ////////////////////////////////////////////////////////////////////////////// + + def bloopConfig(module: JavaModule): Task[BloopConfig.File] = { + import _root_.bloop.config.Config + def out(m: JavaModule) = bloopDir / "out" / m.millModuleSegments.render + def classes(m: JavaModule) = out(m) / "classes" + + val javaConfig = + module.javacOptions.map(opts => Some(Config.Java(options = opts.toList))) + + //////////////////////////////////////////////////////////////////////////// + // Scalac + //////////////////////////////////////////////////////////////////////////// + + val scalaConfig = module match { + case s: ScalaModule => + val semanticDb = s.resolveDeps(s.scalaVersion.map { + case scalaV if semanticDBSupported(scalaV) => + Agg(ivy"org.scalameta:semanticdb-scalac_$scalaV:$semanticDBVersion") + case _ => Agg() + }) + + T.task { + val pluginCp = semanticDb() ++ s.scalacPluginClasspath() + val pluginOptions = pluginCp.map { pathRef => + s"-Xplugin:${pathRef.path}" + } + + val allScalacOptions = + (s.scalacOptions() ++ pluginOptions ++ semanticDBOptions).toList + Some( + BloopConfig.Scala( + organization = "org.scala-lang", + name = "scala-compiler", + version = s.scalaVersion(), + options = allScalacOptions, + jars = s.scalaCompilerClasspath().map(_.path.toNIO).toList, + analysis = None, + setup = None + ) + ) + } + case _ => T.task(None) + } + + //////////////////////////////////////////////////////////////////////////// + // Platform (Jvm/Js/Native) + //////////////////////////////////////////////////////////////////////////// + + val platform = T.task { + BloopConfig.Platform.Jvm( + BloopConfig.JvmConfig( + home = T.ctx().env.get("JAVA_HOME").map(s => Path(s).toNIO), + options = module.forkArgs().toList + ), + mainClass = module.mainClass() + ) + } + + //////////////////////////////////////////////////////////////////////////// + // Tests + //////////////////////////////////////////////////////////////////////////// + + val testConfig = module match { + case m: TestModule => + T.task { + Some( + BloopConfig.Test( + frameworks = m + .testFrameworks() + .map(f => Config.TestFramework(List(f))) + .toList, + options = Config.TestOptions( + excludes = List(), + arguments = List() + ) + ) + ) + } + case _ => T.task(None) + } + + //////////////////////////////////////////////////////////////////////////// + // Ivy dependencies + sources + //////////////////////////////////////////////////////////////////////////// + + val scalaLibraryIvyDeps = module match { + case x: ScalaModule => x.scalaLibraryIvyDeps + case _ => T.task { Loose.Agg.empty[Dep] } + } + + /** + * Resolves artifacts using coursier and creates the corresponding + * bloop config. + */ + def artifacts(repos: Seq[coursier.Repository], + deps: Seq[coursier.Dependency]): List[BloopConfig.Module] = { + import coursier._ + import coursier.util._ + + def source(r: Resolution) = Resolution( + r.dependencies.map(d => + d.copy(attributes = d.attributes.copy(classifier = "sources"))) + ) + + import scala.concurrent.ExecutionContext.Implicits.global + val unresolved = Resolution(deps.toSet) + val fetch = Fetch.from(repos, Cache.fetch[Task]()) + val gatherTask = for { + resolved <- unresolved.process.run(fetch) + resolvedSources <- source(resolved).process.run(fetch) + all = resolved.dependencyArtifacts ++ resolvedSources.dependencyArtifacts + gathered <- Gather[Task].gather(all.distinct.map { + case (dep, art) => Cache.file[Task](art).run.map(dep -> _) + }) + } yield + gathered + .collect { + case (dep, Right(file)) if Path(file).ext == "jar" => + (dep.module.organization, + dep.module.name, + dep.version, + Option(dep.attributes.classifier).filter(_.nonEmpty), + file) + } + .groupBy { + case (org, mod, version, _, _) => (org, mod, version) + } + .mapValues { + _.map { + case (_, mod, _, classifier, file) => + BloopConfig.Artifact(mod, classifier, None, file.toPath) + }.toList + } + .map { + case ((org, mod, version), artifacts) => + BloopConfig.Module( + organization = org, + name = mod, + version = version, + configurations = None, + artifacts = artifacts + ) + } + + gatherTask.unsafeRun().toList + } + + val bloopResolution: Task[BloopConfig.Resolution] = T.task { + val repos = module.repositories + val allIvyDeps = module + .transitiveIvyDeps() ++ scalaLibraryIvyDeps() ++ module.compileIvyDeps() + val coursierDeps = + allIvyDeps.map(module.resolveCoursierDependency()).toList + BloopConfig.Resolution(artifacts(repos, coursierDeps)) + } + + //////////////////////////////////////////////////////////////////////////// + // Classpath + //////////////////////////////////////////////////////////////////////////// + + val ivyDepsClasspath = + module + .resolveDeps(T.task { + module.compileIvyDeps() ++ module.transitiveIvyDeps() + }) + .map(_.map(_.path).toSeq) + + def transitiveClasspath(m: JavaModule): Task[Seq[Path]] = T.task { + m.moduleDeps.map(classes) ++ + m.unmanagedClasspath().map(_.path) ++ + Task.traverse(m.moduleDeps)(transitiveClasspath)().flatten + } + + val classpath = T.task(transitiveClasspath(module)() ++ ivyDepsClasspath()) + val resources = T.task(module.resources().map(_.path.toNIO).toList) + + //////////////////////////////////////////////////////////////////////////// + // Tying up + //////////////////////////////////////////////////////////////////////////// + + val project = T.task { + val mSources = moduleSourceMap() + .get(name(module)) + .toSeq + .flatten + .map(_.toNIO) + .toList + + BloopConfig.Project( + name = name(module), + directory = module.millSourcePath.toNIO, + sources = mSources, + dependencies = module.moduleDeps.map(name).toList, + classpath = classpath().map(_.toNIO).toList, + out = out(module).toNIO, + classesDir = classes(module).toNIO, + resources = Some(resources()), + `scala` = scalaConfig(), + java = javaConfig(), + sbt = None, + test = testConfig(), + platform = Some(platform()), + resolution = Some(bloopResolution()) + ) + } + + T.task { + BloopConfig.File( + version = BloopConfig.File.LatestVersion, + project = project() + ) + } + } + + lazy val millDiscover = Discover[this.type] +} diff --git a/contrib/bloop/src/mill.contrib.bloop/CirceCompat.scala b/contrib/bloop/src/mill.contrib.bloop/CirceCompat.scala new file mode 100644 index 00000000..bfd88e07 --- /dev/null +++ b/contrib/bloop/src/mill.contrib.bloop/CirceCompat.scala @@ -0,0 +1,23 @@ +package mill.contrib.bloop + +import io.circe.{Decoder, Encoder, Json} +import upickle.core.Visitor +import upickle.default + +trait CirceCompat { + + // Converts from a Circe encoder to a uPickle one + implicit def circeWriter[T: Encoder]: default.Writer[T] = + new default.Writer[T] { + override def write0[V](out: Visitor[_, V], v: T) = + ujson.circe.CirceJson.transform(Encoder[T].apply(v), out) + } + + // Converts from a Circe decoder to a uPickle one + implicit def circeReader[T: Decoder]: default.Reader[T] = + new default.Reader.Delegate[Json, T]( + ujson.circe.CirceJson.map(Decoder[T].decodeJson).map(_.right.get)) + +} + +object CirceCompat extends CirceCompat diff --git a/contrib/bloop/src/mill/contrib/Bloop.scala b/contrib/bloop/src/mill/contrib/Bloop.scala new file mode 100644 index 00000000..9c85a308 --- /dev/null +++ b/contrib/bloop/src/mill/contrib/Bloop.scala @@ -0,0 +1,10 @@ +package mill.contrib + +import mill.eval.Evaluator +import os.pwd +import mill.contrib.bloop.BloopImpl + +/** + * Usage : `mill mill.contrib.Bloop/install` + */ +object Bloop extends BloopImpl(Evaluator.currentEvaluator.get, pwd) |