summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rw-r--r--contrib/bloop/src/mill.contrib.bloop/BloopImpl.scala356
-rw-r--r--contrib/bloop/src/mill.contrib.bloop/CirceCompat.scala23
-rw-r--r--contrib/bloop/src/mill/contrib/Bloop.scala10
-rw-r--r--contrib/bloop/test/src/mill/contrib/bloop/BloopTests.scala103
-rw-r--r--contrib/playlib/src/mill/playlib/RouterModule.scala2
-rw-r--r--contrib/scoverage/api/src/ScoverageReportWorkerApi.scala7
-rw-r--r--contrib/scoverage/src/ScoverageModule.scala122
-rw-r--r--contrib/scoverage/src/ScoverageReportWorker.scala39
-rw-r--r--contrib/scoverage/test/resources/hello-world/core/src/Greet.scala6
-rw-r--r--contrib/scoverage/test/src/HelloWorldTests.scala107
-rw-r--r--contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala21
11 files changed, 795 insertions, 1 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)
diff --git a/contrib/bloop/test/src/mill/contrib/bloop/BloopTests.scala b/contrib/bloop/test/src/mill/contrib/bloop/BloopTests.scala
new file mode 100644
index 00000000..dfbb346d
--- /dev/null
+++ b/contrib/bloop/test/src/mill/contrib/bloop/BloopTests.scala
@@ -0,0 +1,103 @@
+package mill.contrib.bloop
+
+import bloop.config.Config.{File => BloopFile}
+import bloop.config.ConfigEncoderDecoders._
+import mill._
+import mill.contrib.bloop.CirceCompat._
+import mill.scalalib._
+import mill.util.{TestEvaluator, TestUtil}
+import os.Path
+import upickle.default._
+import utest._
+
+object BloopTests extends TestSuite {
+
+ val workdir = os.pwd / 'target / 'workspace / "bloop"
+ val testEvaluator = TestEvaluator.static(build)
+ val testBloop = new BloopImpl(() => testEvaluator.evaluator, workdir)
+
+ object build extends TestUtil.BaseModule {
+
+ override def millSourcePath = BloopTests.workdir
+
+ object scalaModule extends scalalib.ScalaModule with testBloop.Module {
+ def scalaVersion = "2.12.8"
+ val bloopVersion = "1.2.5"
+ override def mainClass = Some("foo.bar.Main")
+
+ override def ivyDeps = Agg(
+ ivy"ch.epfl.scala::bloop-config:$bloopVersion"
+ )
+ override def scalacOptions = Seq(
+ "-language:higherKinds"
+ )
+
+ object test extends super.Tests {
+ def testFrameworks = Seq("utest.runner.Framework")
+ }
+ }
+
+ }
+
+ def readBloopConf(jsonFile: String) =
+ read[BloopFile](os.read(workdir / ".bloop" / jsonFile))
+
+ def tests: Tests = Tests {
+ 'genBloopTests - {
+
+ testEvaluator(testBloop.install)
+ val scalaModuleConfig = readBloopConf("scalaModule.json")
+ val testModuleConfig = readBloopConf("scalaModule.test.json")
+
+ 'scalaModule - {
+ val p = scalaModuleConfig.project
+ val name = p.name
+ val sources = p.sources.map(Path(_))
+ val options = p.scala.get.options
+ val version = p.scala.get.version
+ val classpath = p.classpath.map(_.toString)
+ val platform = p.platform.get.name
+ val mainCLass = p.platform.get.mainClass.get
+ val resolution = p.resolution.get.modules
+ val sdb = testBloop.semanticDBVersion
+ val sdbOpts = testBloop.semanticDBOptions
+
+ assert(name == "scalaModule")
+ assert(sources == List(workdir / "scalaModule" / "src"))
+ assert(options.contains("-language:higherKinds"))
+ assert(options.exists(_.contains(s"semanticdb-scalac_2.12.8-$sdb.jar")))
+ assert(sdbOpts.forall(options.contains))
+ assert(version == "2.12.8")
+ assert(classpath.exists(_.contains("bloop-config_2.12-1.2.5.jar")))
+ assert(platform == "jvm")
+ assert(mainCLass == "foo.bar.Main")
+
+ val bloopConfigDep = resolution.find(_.name == "bloop-config_2.12").get
+ val artifacts = bloopConfigDep.artifacts
+ assert(bloopConfigDep.version == build.scalaModule.bloopVersion)
+ assert(bloopConfigDep.organization == "ch.epfl.scala")
+ assert(artifacts.map(_.name).distinct == List("bloop-config_2.12"))
+ assert(artifacts.flatMap(_.classifier).contains("sources"))
+ }
+ 'scalaModuleTest - {
+ val p = testModuleConfig.project
+ val name = p.name
+ val sources = p.sources.map(Path(_))
+ val framework = p.test.get.frameworks.head.names.head
+ val dep = p.dependencies.head
+ val mainModuleClasspath = scalaModuleConfig.project.classpath
+ assert(name == "scalaModule.test")
+ assert(sources == List(workdir / "scalaModule" / "test" / "src"))
+ assert(framework == "utest.runner.Framework")
+ assert(dep == "scalaModule")
+ assert(mainModuleClasspath.forall(p.classpath.contains))
+ }
+ 'configAccessTest - {
+ val (accessedConfig, _) =
+ testEvaluator(build.scalaModule.bloop.config).asSuccess.get.value.right.get
+ assert(accessedConfig == scalaModuleConfig)
+ }
+ }
+ }
+
+}
diff --git a/contrib/playlib/src/mill/playlib/RouterModule.scala b/contrib/playlib/src/mill/playlib/RouterModule.scala
index abf3082b..62a4c16b 100644
--- a/contrib/playlib/src/mill/playlib/RouterModule.scala
+++ b/contrib/playlib/src/mill/playlib/RouterModule.scala
@@ -97,4 +97,4 @@ trait RouterModule extends ScalaModule with Version {
override def generatedSources = T {
super.generatedSources() ++ routerClasses()
}
-} \ No newline at end of file
+}
diff --git a/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala b/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala
new file mode 100644
index 00000000..d74e1275
--- /dev/null
+++ b/contrib/scoverage/api/src/ScoverageReportWorkerApi.scala
@@ -0,0 +1,7 @@
+package mill.contrib.scoverage.api
+
+import mill.eval.PathRef
+
+trait ScoverageReportWorkerApi {
+ def htmlReport(sources: Seq[PathRef], dataDir: String, selfDir: String): Unit
+}
diff --git a/contrib/scoverage/src/ScoverageModule.scala b/contrib/scoverage/src/ScoverageModule.scala
new file mode 100644
index 00000000..b96afa34
--- /dev/null
+++ b/contrib/scoverage/src/ScoverageModule.scala
@@ -0,0 +1,122 @@
+package mill
+package contrib
+package scoverage
+
+import coursier.{Cache, MavenRepository}
+import mill.api.Result
+import mill.eval.PathRef
+import mill.util.Ctx
+import mill.scalalib.{DepSyntax, JavaModule, Lib, ScalaModule, TestModule, Dep}
+import mill.moduledefs.Cacher
+
+
+/** Adds targets to a [[mill.scalalib.ScalaModule]] to create test coverage reports.
+ *
+ * This module allows you to generate code coverage reports for Scala projects with
+ * [[https://github.com/scoverage Scoverage]] via the
+ * [[https://github.com/scoverage/scalac-scoverage-plugin scoverage compiler plugin]].
+ *
+ * To declare a module for which you want to generate coverage reports you can
+ * Extends the `mill.contrib.scoverage.ScoverageModule` trait when defining your
+ * Module. Additionally, you must define a submodule that extends the
+ * `ScoverageTests` trait that belongs to your instance of `ScoverageModule`.
+ *
+ * {{{
+ * // You have to replace VERSION
+ * import $ivy.`com.lihaoyi::mill-contrib-buildinfo:VERSION`
+ * import mill.contrib.scoverage.ScoverageModule
+ *
+ * Object foo extends ScoverageModule {
+ * def scalaVersion = "2.11.8"
+ * def scoverageVersion = "1.3.1"
+ *
+ * object test extends ScoverageTests {
+ * def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.5")
+ * def testFrameworks = Seq("org.scalatest.tools.Framework")
+ * }
+ * }
+ * }}}
+ *
+ * In addition to the normal tasks available to your Scala module, Scoverage
+ * Modules introduce a few new tasks and changes the behavior of an existing one.
+ *
+ * - mill foo.scoverage.compile # compiles your module with test instrumentation
+ * # (you don't have to run this manually, running the test task will force its invocation)
+ *
+ * - mill foo.test # tests your project and collects metrics on code coverage
+ * - mill foo.scoverage.htmlReport # uses the metrics collected by a previous test run to generate a coverage report in html format
+ *
+ * The measurement data is available at `out/foo/scoverage/data/`,
+ * And the html report is saved in `out/foo/scoverage/htmlReport/`.
+ */
+trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
+ def scoverageVersion: T[String]
+ private def scoverageRuntimeDep = T {
+ ivy"org.scoverage::scalac-scoverage-runtime:${outer.scoverageVersion()}"
+ }
+ private def scoveragePluginDep = T {
+ ivy"org.scoverage::scalac-scoverage-plugin:${outer.scoverageVersion()}"
+ }
+
+ private def toolsClasspath = T {
+ scoverageReportWorkerClasspath() ++ scoverageClasspath()
+ }
+
+ def scoverageClasspath = T {
+ Lib.resolveDependencies(
+ Seq(Cache.ivy2Local, MavenRepository("https://repo1.maven.org/maven2")),
+ Lib.depToDependency(_, outer.scalaVersion()),
+ Seq(scoveragePluginDep()),
+ ctx = Some(implicitly[mill.util.Ctx.Log])
+ )
+ }
+
+ def scoverageReportWorkerClasspath = T {
+ val workerKey = "MILL_SCOVERAGE_REPORT_WORKER_" + scoverageVersion().replace(".", "_")
+ mill.modules.Util.millProjectModule(
+ workerKey,
+ s"mill-contrib-scoverage-worker-${outer.scoverageVersion()}",
+ repositories,
+ resolveFilter = _.toString.contains("mill-contrib-scoverage-worker")
+ )
+ }
+
+ object scoverage extends ScalaModule {
+ def selfDir = T { T.ctx().dest / os.up / os.up }
+ def dataDir = T { selfDir() / "data" }
+
+ def sources = outer.sources
+ def resources = outer.resources
+ def scalaVersion = outer.scalaVersion()
+ def compileIvyDeps = outer.compileIvyDeps()
+ def ivyDeps = outer.ivyDeps() ++ Agg(outer.scoverageRuntimeDep())
+ def scalacPluginIvyDeps = outer.scalacPluginIvyDeps() ++ Agg(outer.scoveragePluginDep())
+ def scalacOptions = outer.scalacOptions() ++
+ Seq(s"-P:scoverage:dataDir:${dataDir()}")
+
+ def htmlReport() = T.command {
+ ScoverageReportWorkerApi
+ .scoverageReportWorker()
+ .bridge(toolsClasspath().map(_.path))
+ .htmlReport(sources(), dataDir().toString, selfDir().toString)
+ }
+ }
+
+ trait ScoverageTests extends outer.Tests {
+ override def upstreamAssemblyClasspath = T {
+ super.upstreamAssemblyClasspath() ++
+ resolveDeps(T.task{Agg(outer.scoverageRuntimeDep())})()
+ }
+ override def compileClasspath = T {
+ super.compileClasspath() ++
+ resolveDeps(T.task{Agg(outer.scoverageRuntimeDep())})()
+ }
+ override def runClasspath = T {
+ super.runClasspath() ++
+ resolveDeps(T.task{Agg(outer.scoverageRuntimeDep())})()
+ }
+
+ // Need the sources compiled with scoverage instrumentation to run.
+ override def moduleDeps: Seq[JavaModule] = Seq(outer.scoverage)
+ }
+}
diff --git a/contrib/scoverage/src/ScoverageReportWorker.scala b/contrib/scoverage/src/ScoverageReportWorker.scala
new file mode 100644
index 00000000..1aaa31ad
--- /dev/null
+++ b/contrib/scoverage/src/ScoverageReportWorker.scala
@@ -0,0 +1,39 @@
+package mill.contrib.scoverage
+
+import mill.{Agg, T}
+import mill.api.{ClassLoader, Ctx, Result}
+import mill.define.{Discover, ExternalModule, Worker}
+import mill.eval.PathRef
+
+class ScoverageReportWorker {
+ private var scoverageInstanceCache = Option.empty[(Long, api.ScoverageReportWorkerApi)]
+
+ def bridge(classpath: Agg[os.Path])
+ (implicit ctx: Ctx) = {
+ val classloaderSig =
+ classpath.map(p => p.toString().hashCode + os.mtime(p)).sum
+ scoverageInstanceCache match {
+ case Some((sig, bridge)) if sig == classloaderSig => bridge
+ case _ =>
+ val toolsClassPath = classpath.map(_.toIO.toURI.toURL).toVector
+ ctx.log.debug("Loading classes from\n"+toolsClassPath.mkString("\n"))
+ val cl = ClassLoader.create(
+ toolsClassPath,
+ getClass.getClassLoader
+ )
+ val bridge = cl
+ .loadClass("mill.contrib.scoverage.worker.ScoverageReportWorkerImpl")
+ .getDeclaredConstructor()
+ .newInstance()
+ .asInstanceOf[api.ScoverageReportWorkerApi]
+ scoverageInstanceCache = Some((classloaderSig, bridge))
+ bridge
+ }
+ }
+}
+
+object ScoverageReportWorkerApi extends ExternalModule {
+
+ def scoverageReportWorker = T.worker { new ScoverageReportWorker() }
+ lazy val millDiscover = Discover[this.type]
+}
diff --git a/contrib/scoverage/test/resources/hello-world/core/src/Greet.scala b/contrib/scoverage/test/resources/hello-world/core/src/Greet.scala
new file mode 100644
index 00000000..608becc9
--- /dev/null
+++ b/contrib/scoverage/test/resources/hello-world/core/src/Greet.scala
@@ -0,0 +1,6 @@
+object Greet {
+ def greet(name: String, prefix: Option[String]): String = prefix match {
+ case Some(p) => s"Hello, ${p} ${name}!"
+ case None => s"Hello, ${name}!"
+ }
+}
diff --git a/contrib/scoverage/test/src/HelloWorldTests.scala b/contrib/scoverage/test/src/HelloWorldTests.scala
new file mode 100644
index 00000000..98a4201c
--- /dev/null
+++ b/contrib/scoverage/test/src/HelloWorldTests.scala
@@ -0,0 +1,107 @@
+package mill.contrib.scoverage
+
+import mill._
+import mill.api.Result
+import mill.scalalib._
+import mill.util.{TestEvaluator, TestUtil}
+import utest._
+import utest.framework.TestPath
+
+object HelloWorldTests extends utest.TestSuite {
+ val resourcePath = os.pwd / 'contrib / 'scoverage / 'test / 'resources / "hello-world"
+ trait HelloBase extends TestUtil.BaseModule {
+ def millSourcePath = TestUtil.getSrcPathBase() / millOuterCtx.enclosing.split('.')
+ }
+
+ object HelloWorld extends HelloBase {
+ object core extends ScoverageModule {
+ def scalaVersion = "2.12.4"
+ def scoverageVersion = "1.3.1"
+
+ object test extends ScoverageTests {
+ override def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.5")
+ def testFrameworks = Seq("org.scalatest.tools.Framework")
+ }
+ }
+ }
+
+ def workspaceTest[T](m: TestUtil.BaseModule, resourcePath: os.Path = resourcePath)
+ (t: TestEvaluator => T)
+ (implicit tp: TestPath): T = {
+ val eval = new TestEvaluator(m)
+ os.remove.all(m.millSourcePath)
+ os.remove.all(eval.outPath)
+ os.makeDir.all(m.millSourcePath / os.up)
+ os.copy(resourcePath, m.millSourcePath)
+ t(eval)
+ }
+
+ def tests: utest.Tests = utest.Tests {
+ "HelloWorld" - {
+ "core" - {
+ "scoverageVersion" - workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverageVersion)
+
+ assert(
+ result == "1.3.1",
+ evalCount > 0
+ )
+ }
+ "scoverage" - {
+ "ivyDeps" - workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) =
+ eval.apply(HelloWorld.core.scoverage.ivyDeps)
+
+ assert(
+ result == Agg(ivy"org.scoverage::scalac-scoverage-runtime:1.3.1"),
+ evalCount > 0
+ )
+ }
+ "scalacPluginIvyDeps" - workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) =
+ eval.apply(HelloWorld.core.scoverage.scalacPluginIvyDeps)
+
+ assert(
+ result == Agg(ivy"org.scoverage::scalac-scoverage-plugin:1.3.1"),
+ evalCount > 0
+ )
+ }
+ "dataDir" - workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.dataDir)
+
+ assert(
+ result.toString.endsWith("mill/target/workspace/mill/contrib/scoverage/HelloWorldTests/eval/HelloWorld/core/scoverage/dataDir/core/scoverage/data"),
+ evalCount > 0
+ )
+ }
+ }
+ "test" - {
+ "upstreamAssemblyClasspath" - workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.upstreamAssemblyClasspath)
+
+ assert(
+ result.map(_.toString).exists(_.contains("scalac-scoverage-runtime")),
+ evalCount > 0
+ )
+ }
+ "compileClasspath" - workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.compileClasspath)
+
+ assert(
+ result.map(_.toString).exists(_.contains("scalac-scoverage-runtime")),
+ evalCount > 0
+ )
+ }
+ "runClasspath" - TestUtil.disableInJava9OrAbove(workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) = eval.apply(HelloWorld.core.scoverage.runClasspath)
+
+ assert(
+ result.map(_.toString).exists(_.contains("scalac-scoverage-runtime")),
+ evalCount > 0
+ )
+ })
+ }
+ }
+ }
+ }
+}
diff --git a/contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala b/contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala
new file mode 100644
index 00000000..44f506f7
--- /dev/null
+++ b/contrib/scoverage/worker/1.3.1/src/ScoverageReportWorkerImpl.scala
@@ -0,0 +1,21 @@
+package mill.contrib.scoverage.worker
+
+import mill.contrib.scoverage.api.ScoverageReportWorkerApi
+import mill.eval.PathRef
+import _root_.scoverage.Serializer.{ coverageFile, deserialize }
+import _root_.scoverage.IOUtils.{ findMeasurementFiles, invoked }
+import _root_.scoverage.report.ScoverageHtmlWriter
+
+class ScoverageReportWorkerImpl extends ScoverageReportWorkerApi {
+ def htmlReport(sources: Seq[PathRef], dataDir: String, selfDir: String) = {
+ val coverageFileObj = coverageFile(dataDir)
+ val coverage = deserialize(coverageFileObj)
+ coverage(invoked(findMeasurementFiles(dataDir)))
+ val Seq(PathRef(sourceFolderPath, _, _)) = sources
+ val sourceFolders = Seq(sourceFolderPath.toIO)
+ val htmlFolder = new java.io.File(s"${selfDir}/htmlReport")
+ htmlFolder.mkdir()
+ new ScoverageHtmlWriter(sourceFolders, htmlFolder, None)
+ .write(coverage)
+ }
+}