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.scalajslib.ScalaJSModule import mill.scalajslib.api.{JsEnvConfig, ModuleKind} import mill.scalalib._ import mill.scalanativelib.ScalaNativeModule import mill.scalanativelib.api.ReleaseMode 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 => private val bloopDir = wd / ".bloop" /** * Generates bloop configuration files reflecting the build, * under pwd/.bloop. */ def install() = T.command { val res = Task.traverse(computeModules)(_.bloop.writeConfig)() val written = res.map(_._2).map(_.path) // Cleaning up configs that weren't generated in this run. os.list(bloopDir) .filter(_.ext == "json") .filterNot(written.contains) .foreach(os.remove) res } /** * 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 => /** * Allows to tell Bloop whether it should use "fullOptJs" or * "fastOptJs" when compiling. Used exclusively with ScalaJsModules. */ def linkerMode: T[Option[BloopConfig.LinkerMode]] = None 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) } } def asBloop: Option[Module] = jm match { case m: Module => Some(m) case _ => None } } 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 = T.input { val sources = Task.traverse(computeModules) { m => m.allSources.map { paths => m.millModuleSegments.render -> paths.map(_.path) } }() mill.eval.Result.Success(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) //////////////////////////////////////////////////////////////////////////// def jsLinkerMode(m: JavaModule): Task[Config.LinkerMode] = (m.asBloop match { case Some(bm) => T.task(bm.linkerMode()) case None => T.task(None) }).map(_.getOrElse(Config.LinkerMode.Debug)) val platform: Task[BloopConfig.Platform] = module match { case m: ScalaJSModule => T.task { BloopConfig.Platform.Js( BloopConfig.JsConfig.empty.copy( version = m.scalaJSVersion(), mode = jsLinkerMode(m)(), kind = m.moduleKind() match { case ModuleKind.NoModule => Config.ModuleKindJS.NoModule case ModuleKind.CommonJSModule => Config.ModuleKindJS.CommonJSModule }, emitSourceMaps = m.jsEnvConfig() match{ case c: JsEnvConfig.NodeJs => c.sourceMap case _ => false }, jsdom = Some(false), ), mainClass = module.mainClass() ) } case m: ScalaNativeModule => T.task { BloopConfig.Platform.Native( BloopConfig.NativeConfig.empty.copy( version = m.scalaNativeVersion(), mode = m.releaseMode() match { case ReleaseMode.Debug => BloopConfig.LinkerMode.Debug case ReleaseMode.Release => BloopConfig.LinkerMode.Release }, gc = m.nativeGC(), targetTriple = m.nativeTarget(), nativelib = m.nativeLibJar().path.toNIO, clang = m.nativeClang().toNIO, clangpp = m.nativeClangPP().toNIO, options = Config.NativeOptions( m.nativeLinkingOptions().toList, m.nativeCompileOptions().toList ), linkStubs = m.nativeLinkStubs(), ), mainClass = module.mainClass() ) } case _ => 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 = coursier.Classifier("sources")))) .toSeq ) import scala.concurrent.ExecutionContext.Implicits.global val unresolved = Resolution(deps) val fetch = ResolutionProcess.fetch(repos, coursier.cache.Cache.default.fetch) 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) => coursier.cache.Cache.default.file(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.value, classifier.map(_.value), None, file.toPath) }.toList } .map { case ((org, mod, version), artifacts) => BloopConfig.Module( organization = org.value, name = mod.value, 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 scalaLibIvyDeps = module match { case s: ScalaModule => s.scalaLibraryIvyDeps case _ => T.task(Loose.Agg.empty[Dep]) } val ivyDepsClasspath = module .resolveDeps(T.task { module.compileIvyDeps() ++ module .transitiveIvyDeps() ++ scalaLibIvyDeps() }) .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()) .map(_.distinct) 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] }