diff options
31 files changed, 1389 insertions, 397 deletions
@@ -143,7 +143,8 @@ object scalalib extends MillModule { def moduleDeps = Seq(main, scalalib.api) def ivyDeps = Agg( - ivy"org.scala-sbt:test-interface:1.0" + ivy"org.scala-sbt:test-interface:1.0", + ivy"org.scalameta::scalafmt-dynamic:2.0.0-RC6" ) def genTask(m: ScalaModule) = T.task{ @@ -187,7 +188,7 @@ object scalalib extends MillModule { def ivyDeps = Agg( // Keep synchronized with zinc in Versions.scala - ivy"org.scala-sbt::zinc:1.3.0-M1" + ivy"org.scala-sbt::zinc:1.2.5" ) def testArgs = T{Seq( "-DMILL_SCALA_WORKER=" + runClasspath().map(_.path).mkString(",") @@ -287,6 +288,31 @@ object contrib extends MillModule { def moduleDeps = Seq(scalalib) } + object scoverage extends MillModule { + def moduleDeps = Seq(scalalib, scoverage.api) + + def testArgs = T { + val mapping = Map( + "MILL_SCOVERAGE_REPORT_WORKER_1_3_1" -> worker("1.3.1").compile().classes.path + ) + scalalib.worker.testArgs() ++ + scalalib.backgroundwrapper.testArgs() ++ + (for ((k, v) <- mapping) yield s"-D$k=$v") + } + + object api extends MillApiModule { + def moduleDeps = Seq(scalalib) + } + + object worker extends Cross[WorkerModule]("1.3.1") + + class WorkerModule(scoverageVersion: String) extends MillApiModule { + def moduleDeps = Seq(scoverage.api) + + def ivyDeps = Agg(ivy"org.scoverage::scalac-scoverage-plugin:${scoverageVersion}") + } + } + object buildinfo extends MillModule { def moduleDeps = Seq(scalalib) // why do I need this? @@ -307,9 +333,18 @@ object contrib extends MillModule { def ivyDeps = Agg(ivy"org.flywaydb:flyway-core:5.2.4") } + object docker extends MillModule { def moduleDeps = Seq(scalalib) } + + object bloop extends MillModule { + def moduleDeps = Seq(scalalib) + def ivyDeps = Agg( + ivy"ch.epfl.scala::bloop-config:1.2.5", + ivy"com.lihaoyi::ujson-circe:0.7.4" + ) + } } @@ -431,7 +466,7 @@ def launcherScript(shellJvmArgs: Seq[String], } object dev extends MillModule{ - def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, contrib.scalapblib, contrib.tut) + def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, contrib.scalapblib, contrib.tut, contrib.scoverage) def forkArgs = ( diff --git a/ci/test-mill-0.sh b/ci/test-mill-0.sh index fa8d7604..83c361b1 100755 --- a/ci/test-mill-0.sh +++ b/ci/test-mill-0.sh @@ -6,4 +6,5 @@ set -eux git clean -xdf # Run tests -mill -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,main.client,contrib.scalapblib,contrib.flyway}.test + +mill -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,main.client,contrib.scalapblib,contrib.flyway,contrib.scoverage}.test
\ No newline at end of file diff --git a/ci/test-mill-bootstrap.sh b/ci/test-mill-bootstrap.sh index f95c0646..80086df2 100755 --- a/ci/test-mill-bootstrap.sh +++ b/ci/test-mill-bootstrap.sh @@ -27,4 +27,4 @@ git clean -xdf rm -rf ~/.mill # Use second build to run tests using Mill -~/mill-2 -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,contrib.scalapblib}.test +~/mill-2 -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,contrib.scalapblib,contrib.scoverage}.test 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) + } +} diff --git a/docs/pages/10 - Thirdparty Modules.md b/docs/pages/10 - Thirdparty Modules.md new file mode 100644 index 00000000..b98b9f67 --- /dev/null +++ b/docs/pages/10 - Thirdparty Modules.md @@ -0,0 +1,250 @@ + +The modules (aka plugins) in this section are developed/maintained outside the mill git tree. + +Besides the documentation provided here, we urge you to consult the respective linked plugin documentation pages. +The usage examples given here are most probably incomplete and sometimes outdated. + +If you develop or maintain a mill plugin, please create a [pull request](https://github.com/lihaoyi/mill/pulls) to get your plugin listed here. + +[comment]: # (Please keep list of plugins in alphabetical order) + +## DGraph + +Show transitive dependencies of your build in your browser. + +Project home: https://github.com/ajrnz/mill-dgraph + +### Quickstart + +```scala +import $ivy.`com.github.ajrnz::mill-dgraph:0.2.0` +``` + +```sh +sh> mill plugin.dgraph.browseDeps(proj)() +``` + +## Ensime + +Create an [.ensime](http://ensime.github.io/ "ensime") file for your build. + +Project home: https://github.com/yyadavalli/mill-ensime + +### Quickstart + +```scala +import $ivy.`fun.valycorp::mill-ensime:0.0.1` +``` + +```sh +sh> mill fun.valycorp.mill.GenEnsime/ensimeConfig +``` + +## Integration Testing Mill Plugins + +Integration testing for mill plugins. + +### Quickstart + +We assume, you have a mill plugin named `mill-demo` + +```scala +// build.sc +import mill._, mill.scalalib._ +object demo extends ScalaModule with PublishModule { + // ... +} +``` + +Add an new test sub-project, e.g. `it`. + +```scala +import $ivy.`de.tototec::de.tobiasroeser.mill.integrationtest:0.1.0` +import de.tobiasroeser.mill.integrationtest._ + +object it extends MillIntegrationTest { + + def millTestVersion = "{exampleMillVersion}" + + def pluginsUnderTest = Seq(demo) + +} +``` + +Your project should now look similar to this: + +```text +. ++-- demo/ +| +-- src/ +| ++-- it/ + +-- src/ + +-- 01-first-test/ + | +-- build.sc + | +-- src/ + | + +-- 02-second-test/ + +-- build.sc +``` + +As the buildfiles `build.sc` in your test cases typically want to access the locally built plugin(s), +the plugins publishes all plugins referenced under `pluginsUnderTest` to a temporary ivy repository, just before the test is executed. +The mill version used in the integration test then used that temporary ivy repository. + +Instead of referring to your plugin with `import $ivy.'your::plugin:version'`, +you can use the following line instead, which ensures you will use the correct locally build plugins. + +```scala +// build.sc +import $exec.plugins +``` + +Effectively, at execution time, this line gets replaced by the content of `plugins.sc`, a file which was generated just before the test started to execute. + +### Configuration and Targets + +The mill-integrationtest plugin provides the following targets. + +#### Mandatory configuration + +* `def millTestVersion: T[String]` + The mill version used for executing the test cases. + Used by `downloadMillTestVersion` to automatically download. + +* `def pluginsUnderTest: Seq[PublishModule]` - + The plugins used in the integration test. + You should at least add your plugin under test here. + You can also add additional libraries, e.g. those that assist you in the test result validation (e.g. a local test support project). + The defined modules will be published into a temporary ivy repository before the tests are executed. + In your test `build.sc` file, instead of the typical `import $ivy.` line, + you should use `import $exec.plugins` to include all plugins that are defined here. + +#### Optional configuration + +* `def sources: Sources` - + Locations where integration tests are located. + Each integration test is a sub-directory, containing a complete test mill project. + +* `def testCases: T[Seq[PathRef]]` - + The directories each representing a mill test case. + Derived from `sources`. + +* `def testTargets: T[Seq[String]]` - + The targets which are called to test the project. + Defaults to `verify`, which should implement test result validation. + +* `def downloadMillTestVersion: T[PathRef]` - + Download the mill version as defined by `millTestVersion`. + Override this, if you need to use a custom built mill version. + Returns the `PathRef` to the mill executable (must have the executable flag). + +#### Commands + +* `def test(): Command[Unit]` - + Run the integration tests. + + +## JBake + +Create static sites/blogs with JBake. + +Plugin home: https://github.com/lefou/mill-jbake + +JBake home: https://jbake.org + +### Quickstart + +```scala +// build.sc +import mill._ +import $ivy.`de.tototec::de.tobiasroeser.mill.jbake:0.1.0` +import de.tobiasroeser.mill.jbake._ + +object site extends JBakeModule { + + def jbakeVersion = "2.6.4" + +} +``` + +Generate the site: + +```sh +bash> mill site.jbake +``` + +Start a local Web-Server on Port 8820 with the generated site: + +```sh +bash> mill site.jbakeServe +``` + + +## OSGi + +Produce OSGi Bundles with mill. + +Project home: https://github.com/lefou/mill-osgi + +### Quickstart + +```scala +import mill._, mill.scalalib._ +import $ivy.`de.tototec::de.tobiasroeser.mill.osgi:0.0.5` +import de.tobiasroeser.mill.osgi._ + +object project extends ScalaModule with OsgiBundleModule { + + def bundleSymbolicName = "com.example.project" + + def osgiHeaders = T{ super.osgiHeaders().copy( + `Export-Package` = Seq("com.example.api"), + `Bundle-Activator` = Some("com.example.internal.Activator") + )} + + // other settings ... + +} +``` + + +## PublishM2 + +Mill plugin to publish artifacts into a local Maven repository. + +Project home: https://github.com/lefou/mill-publishM2 + +### Quickstart + +Just mix-in the `PublishM2Module` into your project. +`PublishM2Module` already extends mill's built-in `PublishModule`. + +File: `build.sc` +```scala +import mill._, scalalib._, publish._ + +import $ivy.`de.tototec::de.tobiasroeser.mill.publishM2:0.0.1` +import de.tobiasroeser.mill.publishM2._ + +object project extends PublishModule with PublishM2Module { + // ... +} +``` + +Publishing to default local Maven repository + +```bash +> mill project.publishM2Local +[40/40] project.publishM2Local +Publishing to /home/user/.m2/repository +``` + +Publishing to custom local Maven repository + +```bash +> mill project.publishM2Local /tmp/m2repo +[40/40] project.publishM2Local +Publishing to /tmp/m2repo +``` + diff --git a/docs/pages/2 - Configuring Mill.md b/docs/pages/2 - Configuring Mill.md index dbcbcee4..f6ca86a0 100644 --- a/docs/pages/2 - Configuring Mill.md +++ b/docs/pages/2 - Configuring Mill.md @@ -237,6 +237,12 @@ Now you can reformat code with `mill foo.reformat` command. You can also reformat your project's code globally with `mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources` command. It will reformat all sources that matches `__.sources` query. +If you add a `.scalafmt.conf` file at the root of you project, it will be used +to configure formatting. It can contain a `version` key to specify the scalafmt +version used to format your code. See the +[scalafmt configuration documentation](https://scalameta.org/scalafmt/docs/configuration.html) +for details. + ## Common Configuration ```scala diff --git a/docs/pages/9 - Contrib Modules.md b/docs/pages/9 - Contrib Modules.md index 9adddcc5..36eb40ef 100644 --- a/docs/pages/9 - Contrib Modules.md +++ b/docs/pages/9 - Contrib Modules.md @@ -1,8 +1,69 @@ -## Contrib Modules The plugins in this section are developed/maintained in the mill git tree. -### BuildInfo +When using one of these, you should make sure to use the versions that matches your mill version. + +[comment]: # (Please keep list of plugins in alphabetical order) + +## Bloop + +This plugin generates [bloop](https://scalacenter.github.io/bloop/) configuration +from your build file, which lets you use the bloop CLI for compiling, and makes +your scala code editable in [Metals](https://scalameta.org/metals/) + + +### Quickstart +```scala +// build.sc (or any other .sc file it depends on, including predef) +// Don't forget to replace VERSION +import $ivy.`com.lihaoyi::mill-contrib-bloop:VERSION` +``` + +Then in your terminal : + +``` +> mill mill.contrib.Bloop/install +``` + +### Mix-in + +You can mix-in the `Bloop.Module` trait with any JavaModule to quickly access +the deserialised configuration for that particular module: + +```scala +// build.sc +import mill._ +import mill.scalalib._ +import mill.contrib.Bloop + +object MyModule extends ScalaModule with Bloop.Module { + def myTask = T { bloop.config() } +} +``` + +### Note regarding metals + +Generating the bloop config should be enough for metals to pick it up and for +features to start working in vscode (or the bunch of other editors metals supports). +However, note that this applies only to your project sources. Your mill/ammonite related +`.sc` files are not yet supported by metals. + +The generated bloop config references the semanticDB compiler plugin required by +metals to function. If need be, the version of semanticDB can be overriden by +extending `mill.contrib.bloop.BloopImpl` in your own space. + +### Note regarding current mill support in bloop + +The mill-bloop integration currently present in the [bloop codebase](https://github.com/scalacenter/bloop/blob/master/integrations/mill-bloop/src/main/scala/bloop/integrations/mill/MillBloop.scala#L10) +will be deprecated in favour of this implementation. + +### Caveats + +At this time, only Java/ScalaModule are processed correctly. ScalaJS/ScalaNative integration will +be added in a near future. + + +## BuildInfo Generate scala code from your buildfile. This plugin generates a single object containing information from your build. @@ -27,7 +88,7 @@ object project extends BuildInfo { } ``` -#### Configuration options +### Configuration options * `def buildInfoMembers: T[Map[String, String]]` The map containing all member names and values for the generated info object. @@ -83,7 +144,7 @@ object docker extends DockerConfig { Run mill in interactive mode to see the docker client output, like `mill -i foo.docker.build`. -### Flyway +## Flyway Enables you to configure and run [Flyway](https://flywaydb.org/) commands from your mill build file. The flyway module currently supports the most common flyway use cases with file based migrations. @@ -127,7 +188,7 @@ mill foo.flywayMigrate > You should write some code to populate the settings for flyway instead. > For example `def flywayPassword = T.input(T.ctx().env("FLYWAY_PASSWORD"))` -### Play Framework +## Play Framework This module adds basic Play Framework support to mill: @@ -140,7 +201,7 @@ This module adds basic Play Framework support to mill: There is no specific Play Java support, building a Play Java application will require a bit of customization (mostly adding the proper dependencies). -#### Using the plugin +### Using the plugin There are 2 base modules and 2 helper traits in this plugin, all of which can be found in `mill.playlib`. @@ -162,7 +223,7 @@ directories at the top level alongside the `build.sc` file. This trait takes car * `RouterModule` allows you to use the Play router without the rest of the configuration (see [Using the router module directly](#using-the-router-module-directly).) -#### Using `PlayModule` +### Using `PlayModule` In order to use the `PlayModule` for your application, you need to provide the scala, Play and Twirl versions. You also need to define your own test object which extends the provided @@ -229,7 +290,7 @@ In order to have a working `start` command the following runtime dependency is a ivy"com.typesafe.play::play-akka-http-server:${playVersion()}" ``` -#### Using `PlayApiModule` +### Using `PlayApiModule` The `PlayApiModule` trait behaves the same as the `PlayModule` trait but it won't process .scala .html files and you don't need to define the `twirlVersion: @@ -250,12 +311,12 @@ object core extends PlayApiModule { } ``` -#### Play configuration options +### Play configuration options The Play modules themselves don't have specific configuration options at this point but the [router module configuration options](#router-configuration-options) and the [Twirl module configuration options](#twirl-configuration-options) are applicable. -#### Additional play libraries +### Additional play libraries The following helpers are available to provide additional Play Framework dependencies: @@ -290,7 +351,7 @@ object core extends PlayApiModule { } ``` -#### Commands equivalence +### Commands equivalence Mill commands are targets on a named build. For example if your build is called `core`: @@ -306,7 +367,7 @@ starts a server in *PROD* mode which: command to get a runnable fat jar of the project. The packaging is slightly different but should be find for a production deployment. -#### Using `SingleModule` +### Using `SingleModule` The `SingleModule` trait allows you to have the build descriptor at the same level as the source code on the filesystem. You can move from there to a multi-module build either by refactoring @@ -394,7 +455,7 @@ the layout becomes: └── controllers ``` -##### Using the router module directly +#### Using the router module directly If you want to use the router module in a project which doesn't use the default Play layout, you can mix-in the `mill.playlib.routesModule` trait directly when defining your module. Your app must @@ -413,7 +474,7 @@ object app extends ScalaModule with RouterModule { } ``` -###### Router Configuration options +##### Router Configuration options * `def playVersion: T[String]` (mandatory) - The version of Play to use to compile the routes file. * `def scalaVersion: T[String]` - The scalaVersion in use in your project. @@ -426,7 +487,7 @@ object app extends ScalaModule with RouterModule { * `def generatorType: RouteCompilerType = RouteCompilerType.InjectedGenerator` - The routes compiler type, one of RouteCompilerType.InjectedGenerator or RouteCompilerType.StaticGenerator -###### Details +##### Details The following filesystem layout is expected by default: @@ -467,7 +528,7 @@ object app extends ScalaModule with RouterModule { ``` -### ScalaPB +## ScalaPB This module allows [ScalaPB](https://scalapb.github.io) to be used in Mill builds. ScalaPB is a [Protocol Buffers](https://developers.google.com/protocol-buffers/) compiler plugin that generates Scala case classes, encoders and decoders for protobuf messages. @@ -498,7 +559,7 @@ example/ resources/ ``` -#### Configuration options +### Configuration options * scalaPBVersion (mandatory) - The ScalaPB version `String` e.g. `"0.7.4"` @@ -526,7 +587,50 @@ object example extends ScalaPBModule { } ``` -### TestNG + +## Scoverage + +This module allows you to generate code coverage reports for Scala projects with +[Scoverage](https://github.com/scoverage) via the +[scalac-scoverage-plugin](https://github.com/scoverage/scalac-scoverage-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`. + +```scala +// 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/`. + + +## TestNG Provides support for [TestNG](https://testng.org/doc/index.html). @@ -543,7 +647,7 @@ object project extends ScalaModule { } ``` -### Tut +## Tut This module allows [Tut](https://tpolecat.github.io/tut) to be used in Mill builds. Tut is a documentation tool which compiles and evaluates Scala code in documentation files and provides various options for configuring how the results will be displayed in the compiled documentation. @@ -582,7 +686,7 @@ In order to compile documentation we can execute the `tut` task in the module: sh> mill example.tut ``` -#### Configuration options +### Configuration options * tutSourceDirectory - This task determines where documentation files must be placed in order to be compiled with Tut. By default this is the `tut` folder at the root of the module. @@ -602,7 +706,7 @@ sh> mill example.tut * tutPluginJars - A task which performs the dependency resolution for the scalac plugins to be used with Tut. -### Twirl +## Twirl Twirl templates support. @@ -621,7 +725,7 @@ object app extends ScalaModule with TwirlModule { } ``` -#### Twirl configuration options +### Twirl configuration options * `def twirlVersion: T[String]` (mandatory) - the version of the twirl compiler to use, like "1.3.15" * `def twirlAdditionalImports: Seq[String] = Nil` - the additional imports that will be added by twirl compiler to the top of all templates @@ -629,7 +733,7 @@ object app extends ScalaModule with TwirlModule { * `def twirlCodec = Codec(Properties.sourceEncoding)` - the codec used to generate the files (the default is the same sbt plugin uses) * `def twirlInclusiveDot: Boolean = false` -#### Details +### Details The following filesystem layout is expected: @@ -703,256 +807,5 @@ Seq( These imports will always be added to every template. You don't need to list them if you override `twirlAdditionalImports`. -#### Example +### Example There's an [example project](https://github.com/lihaoyi/cask/tree/master/example/twirl) - - - -## Thirdparty Mill Plugins - -The plugins in this section are developed/maintained outside the mill git tree. - -Besides the documentation provided here, we urge you to consult the respective linked plugin documentation pages. -The usage examples given here are most probably outdated and incomplete. - -If you develop or maintain a mill plugin, please create a [pull request](https://github.com/lihaoyi/mill/pulls) to get your plugin listed here. - -### DGraph - -Show transitive dependencies of your build in your browser. - -Project home: https://github.com/ajrnz/mill-dgraph - -#### Quickstart - -```scala -import $ivy.`com.github.ajrnz::mill-dgraph:0.2.0` -``` - -```sh -sh> mill plugin.dgraph.browseDeps(proj)() -``` - -### Ensime - -Create an [.ensime](http://ensime.github.io/ "ensime") file for your build. - -Project home: https://github.com/yyadavalli/mill-ensime - -#### Quickstart - -```scala -import $ivy.`fun.valycorp::mill-ensime:0.0.1` -``` - -```sh -sh> mill fun.valycorp.mill.GenEnsime/ensimeConfig -``` - -### Integration Testing Mill Plugins - -Integration testing for mill plugins. - -#### Quickstart - -We assume, you have a mill plugin named `mill-demo` - -```scala -// build.sc -import mill._, mill.scalalib._ -object demo extends ScalaModule with PublishModule { - // ... -} -``` - -Add an new test sub-project, e.g. `it`. - -```scala -import $ivy.`de.tototec::de.tobiasroeser.mill.integrationtest:0.1.0` -import de.tobiasroeser.mill.integrationtest._ - -object it extends MillIntegrationTest { - - def millTestVersion = "{exampleMillVersion}" - - def pluginsUnderTest = Seq(demo) - -} -``` - -Your project should now look similar to this: - -```text -. -+-- demo/ -| +-- src/ -| -+-- it/ - +-- src/ - +-- 01-first-test/ - | +-- build.sc - | +-- src/ - | - +-- 02-second-test/ - +-- build.sc -``` - -As the buildfiles `build.sc` in your test cases typically want to access the locally built plugin(s), -the plugins publishes all plugins referenced under `pluginsUnderTest` to a temporary ivy repository, just before the test is executed. -The mill version used in the integration test then used that temporary ivy repository. - -Instead of referring to your plugin with `import $ivy.'your::plugin:version'`, -you can use the following line instead, which ensures you will use the correct locally build plugins. - -```scala -// build.sc -import $exec.plugins -``` - -Effectively, at execution time, this line gets replaced by the content of `plugins.sc`, a file which was generated just before the test started to execute. - -#### Configuration and Targets - -The mill-integrationtest plugin provides the following targets. - -##### Mandatory configuration - -* `def millTestVersion: T[String]` - The mill version used for executing the test cases. - Used by `downloadMillTestVersion` to automatically download. - -* `def pluginsUnderTest: Seq[PublishModule]` - - The plugins used in the integration test. - You should at least add your plugin under test here. - You can also add additional libraries, e.g. those that assist you in the test result validation (e.g. a local test support project). - The defined modules will be published into a temporary ivy repository before the tests are executed. - In your test `build.sc` file, instead of the typical `import $ivy.` line, - you should use `import $exec.plugins` to include all plugins that are defined here. - -##### Optional configuration - -* `def sources: Sources` - - Locations where integration tests are located. - Each integration test is a sub-directory, containing a complete test mill project. - -* `def testCases: T[Seq[PathRef]]` - - The directories each representing a mill test case. - Derived from `sources`. - -* `def testTargets: T[Seq[String]]` - - The targets which are called to test the project. - Defaults to `verify`, which should implement test result validation. - -* `def downloadMillTestVersion: T[PathRef]` - - Download the mill version as defined by `millTestVersion`. - Override this, if you need to use a custom built mill version. - Returns the `PathRef` to the mill executable (must have the executable flag). - -##### Commands - -* `def test(): Command[Unit]` - - Run the integration tests. - - -### JBake - -Create static sites/blogs with JBake. - -Plugin home: https://github.com/lefou/mill-jbake - -JBake home: https://jbake.org - -#### Quickstart - -```scala -// build.sc -import mill._ -import $ivy.`de.tototec::de.tobiasroeser.mill.jbake:0.1.0` -import de.tobiasroeser.mill.jbake._ - -object site extends JBakeModule { - - def jbakeVersion = "2.6.4" - -} -``` - -Generate the site: - -```sh -bash> mill site.jbake -``` - -Start a local Web-Server on Port 8820 with the generated site: - -```sh -bash> mill site.jbakeServe -``` - - -### OSGi - -Produce OSGi Bundles with mill. - -Project home: https://github.com/lefou/mill-osgi - -#### Quickstart - -```scala -import mill._, mill.scalalib._ -import $ivy.`de.tototec::de.tobiasroeser.mill.osgi:0.0.5` -import de.tobiasroeser.mill.osgi._ - -object project extends ScalaModule with OsgiBundleModule { - - def bundleSymbolicName = "com.example.project" - - def osgiHeaders = T{ super.osgiHeaders().copy( - `Export-Package` = Seq("com.example.api"), - `Bundle-Activator` = Some("com.example.internal.Activator") - )} - - // other settings ... - -} -``` - - -### PublishM2 - -Mill plugin to publish artifacts into a local Maven repository. - -Project home: https://github.com/lefou/mill-publishM2 - -#### Quickstart - -Just mix-in the `PublishM2Module` into your project. -`PublishM2Module` already extends mill's built-in `PublishModule`. - -File: `build.sc` -```scala -import mill._, scalalib._, publish._ - -import $ivy.`de.tototec::de.tobiasroeser.mill.publishM2:0.0.1` -import de.tobiasroeser.mill.publishM2._ - -object project extends PublishModule with PublishM2Module { - // ... -} -``` - -Publishing to default local Maven repository - -```bash -> mill project.publishM2Local -[40/40] project.publishM2Local -Publishing to /home/user/.m2/repository -``` - -Publishing to custom local Maven repository - -```bash -> mill project.publishM2Local /tmp/m2repo -[40/40] project.publishM2Local -Publishing to /tmp/m2repo -``` diff --git a/main/src/MillMain.scala b/main/src/MillMain.scala index f1a7a9e7..ca4af87d 100644 --- a/main/src/MillMain.scala +++ b/main/src/MillMain.scala @@ -23,54 +23,57 @@ object MillMain { System.out, System.err, System.getenv().asScala.toMap, - b => () + b => (), + initialSystemProperties = Map() ) - System.exit(if(result) 0 else 1) + System.exit(if (result) 0 else 1) } - def main0(args: Array[String], - stateCache: Option[Evaluator.State], - mainInteractive: Boolean, - stdin: InputStream, - stdout: PrintStream, - stderr: PrintStream, - env: Map[String, String], - setIdle: Boolean => Unit): (Boolean, Option[Evaluator.State]) = { + def main0( + args: Array[String], + stateCache: Option[Evaluator.State], + mainInteractive: Boolean, + stdin: InputStream, + stdout: PrintStream, + stderr: PrintStream, + env: Map[String, String], + setIdle: Boolean => Unit, + initialSystemProperties: Map[String, String] + ): (Boolean, Option[Evaluator.State]) = { import ammonite.main.Cli - + val millHome = mill.api.Ctx.defaultHome val removed = Set("predef-code", "no-home-predef") + var interactive = false val interactiveSignature = Arg[Config, Unit]( "interactive", Some('i'), "Run Mill in interactive mode, suitable for opening REPLs and taking user input. In this mode, no mill server will be used.", - (c, v) =>{ + (c, v) => { interactive = true c } ) - - var disableTicker = false val disableTickerSignature = Arg[Config, Unit]( - "disable-ticker", None, - "Disable ticker log (e.g. short-lived prints of stages and progress bars)", - (c, v) =>{ - disableTicker = true - c - } + name = "disable-ticker", shortName = None, + doc = "Disable ticker log (e.g. short-lived prints of stages and progress bars)", + action = (c, v) => { + disableTicker = true + c + } ) var debugLog = false val debugLogSignature = Arg[Config, Unit]( name = "debug", shortName = Some('d'), doc = "Show debug output on STDOUT", - (c, v) => { - debugLog = true - c - } + action = (c, v) => { + debugLog = true + c + } ) var keepGoing = false @@ -82,27 +85,46 @@ object MillMain { } ) + var extraSystemProperties = Map[String, String]() + val extraSystemPropertiesSignature = Arg[Config, String]( + name = "define", shortName = Some('D'), + doc = "Define (or overwrite) a system property", + action = { (c, v) => + extraSystemProperties += (v.split("[=]", 2) match { + case Array(k, v) => k -> v + case Array(k) => k -> "" + }) + c + } + ) + val millArgSignature = Cli.genericSignature.filter(a => !removed(a.name)) ++ - Seq(interactiveSignature, disableTickerSignature, debugLogSignature, keepGoingSignature) + Seq( + interactiveSignature, + disableTickerSignature, + debugLogSignature, + keepGoingSignature, + extraSystemPropertiesSignature + ) Cli.groupArgs( args.toList, millArgSignature, Cli.Config(home = millHome, remoteLogging = false) - ) match{ - case _ if interactive => - stderr.println("-i/--interactive must be passed in as the first argument") - (false, None) - case Left(msg) => - stderr.println(msg) - (false, None) - case Right((cliConfig, _)) if cliConfig.help => - val leftMargin = millArgSignature.map(ammonite.main.Cli.showArg(_).length).max + 2 - stdout.println( - s"""Mill Build Tool - |usage: mill [mill-options] [target [target-options]] - | + ) match { + case _ if interactive => + stderr.println("-i/--interactive must be passed in as the first argument") + (false, None) + case Left(msg) => + stderr.println(msg) + (false, None) + case Right((cliConfig, _)) if cliConfig.help => + val leftMargin = millArgSignature.map(ammonite.main.Cli.showArg(_).length).max + 2 + stdout.println( + s"""Mill Build Tool + |usage: mill [mill-options] [target [target-options]] + | |${formatBlock(millArgSignature, leftMargin).mkString(ammonite.util.Util.newLine)}""".stripMargin ) (true, None) @@ -113,6 +135,8 @@ object MillMain { stderr.println("Build repl needs to be run with the -i/--interactive flag") (false, stateCache) }else{ + val systemProps = initialSystemProperties ++ extraSystemProperties + val config = if(!repl) cliConfig else cliConfig.copy( @@ -125,43 +149,45 @@ object MillMain { | repl.pprinter(), | build.millSelf.get, | build.millDiscover, - | $debugLog, - | keepGoing = $keepGoing + | debugLog = $debugLog, + | keepGoing = $keepGoing, + | systemProperties = ${systemProps} |) |repl.pprinter() = replApplyHandler.pprinter |import replApplyHandler.generatedEval._ | """.stripMargin, - welcomeBanner = None + welcomeBanner = None + ) + + val runner = new mill.main.MainRunner( + config.copy(colored = config.colored orElse Option(mainInteractive)), + disableTicker, + stdout, stderr, stdin, + stateCache, + env, + setIdle, + debugLog = debugLog, + keepGoing = keepGoing, + systemProperties = systemProps ) - val runner = new mill.main.MainRunner( - config.copy(colored = config.colored orElse Option(mainInteractive)), - disableTicker, - stdout, stderr, stdin, - stateCache, - env, - setIdle, - debugLog, - keepGoing = keepGoing - ) - - if (mill.main.client.Util.isJava9OrAbove) { - val rt = cliConfig.home / Export.rtJarName - if (!os.exists(rt)) { - runner.printInfo(s"Preparing Java ${System.getProperty("java.version")} runtime; this may take a minute or two ...") - Export.rtTo(rt.toIO, false) + if (mill.main.client.Util.isJava9OrAbove) { + val rt = cliConfig.home / Export.rtJarName + if (!os.exists(rt)) { + runner.printInfo(s"Preparing Java ${System.getProperty("java.version")} runtime; this may take a minute or two ...") + Export.rtTo(rt.toIO, false) + } } - } - if (repl){ - runner.printInfo("Loading...") - (runner.watchLoop(isRepl = true, printing = false, _.run()), runner.stateCache) - } else { - (runner.runScript(os.pwd / "build.sc", leftoverArgs), runner.stateCache) + if (repl) { + runner.printInfo("Loading...") + (runner.watchLoop(isRepl = true, printing = false, _.run()), runner.stateCache) + } else { + (runner.runScript(os.pwd / "build.sc", leftoverArgs), runner.stateCache) + } } - } - } + } } } diff --git a/main/src/main/MainRunner.scala b/main/src/main/MainRunner.scala index e08905a6..c773087c 100644 --- a/main/src/main/MainRunner.scala +++ b/main/src/main/MainRunner.scala @@ -25,7 +25,8 @@ class MainRunner(val config: ammonite.main.Cli.Config, env : Map[String, String], setIdle: Boolean => Unit, debugLog: Boolean, - keepGoing: Boolean) + keepGoing: Boolean, + systemProperties: Map[String, String]) extends ammonite.MainRunner( config, outprintStream, errPrintStream, stdIn, outprintStream, errPrintStream @@ -85,7 +86,8 @@ class MainRunner(val config: ammonite.main.Cli.Config, debugEnabled = debugLog ), env, - keepGoing = keepGoing + keepGoing = keepGoing, + systemProperties ) result match{ diff --git a/main/src/main/MillServerMain.scala b/main/src/main/MillServerMain.scala index 862daaf7..500c3e8f 100644 --- a/main/src/main/MillServerMain.scala +++ b/main/src/main/MillServerMain.scala @@ -21,7 +21,8 @@ trait MillServerMain[T]{ stdout: PrintStream, stderr: PrintStream, env : Map[String, String], - setIdle: Boolean => Unit): (Boolean, Option[T]) + setIdle: Boolean => Unit, + systemProperties: Map[String, String]): (Boolean, Option[T]) } object MillServerMain extends mill.main.MillServerMain[Evaluator.State]{ @@ -44,6 +45,7 @@ object MillServerMain extends mill.main.MillServerMain[Evaluator.State]{ mill.main.client.Locks.files(args0(0)) ).run() } + def main0(args: Array[String], stateCache: Option[Evaluator.State], mainInteractive: Boolean, @@ -51,7 +53,8 @@ object MillServerMain extends mill.main.MillServerMain[Evaluator.State]{ stdout: PrintStream, stderr: PrintStream, env : Map[String, String], - setIdle: Boolean => Unit) = { + setIdle: Boolean => Unit, + systemProperties: Map[String, String]) = { MillMain.main0( args, stateCache, @@ -60,7 +63,8 @@ object MillServerMain extends mill.main.MillServerMain[Evaluator.State]{ stdout, stderr, env, - setIdle = setIdle + setIdle = setIdle, + systemProperties ) } } @@ -132,6 +136,7 @@ class Server[T](lockBase: String, } val args = Util.parseArgs(argStream) val env = Util.parseMap(argStream) + val systemProperties = Util.parseMap(argStream) argStream.close() @volatile var done = false @@ -147,6 +152,7 @@ class Server[T](lockBase: String, stderr, env.asScala.toMap, idle = _, + systemProperties.asScala.toMap ) sm.stateCache = newStateCache diff --git a/main/src/main/ReplApplyHandler.scala b/main/src/main/ReplApplyHandler.scala index 6f1e060d..7f959929 100644 --- a/main/src/main/ReplApplyHandler.scala +++ b/main/src/main/ReplApplyHandler.scala @@ -1,14 +1,13 @@ package mill.main +import scala.collection.mutable +import mill.api.Strict.Agg import mill.define.Applicative.ApplyHandler import mill.define.Segment.Label import mill.define._ import mill.eval.{Evaluator, Result} -import mill.api.Strict.Agg - -import scala.collection.mutable object ReplApplyHandler{ def apply[T](home: os.Path, disableTicker: Boolean, @@ -17,7 +16,8 @@ object ReplApplyHandler{ rootModule: mill.define.BaseModule, discover: Discover[_], debugLog: Boolean, - keepGoing: Boolean) = { + keepGoing: Boolean, + systemProperties: Map[String, String]): ReplApplyHandler = { new ReplApplyHandler( pprinter0, new Evaluator( @@ -36,7 +36,8 @@ object ReplApplyHandler{ debugEnabled = debugLog ), failFast = !keepGoing - ) + ), + systemProperties ) } def pprintCross(c: mill.define.Cross[_], evaluator: Evaluator) = { @@ -113,8 +114,15 @@ object ReplApplyHandler{ } } + class ReplApplyHandler(pprinter0: pprint.PPrinter, - val evaluator: Evaluator) extends ApplyHandler[Task] { + val evaluator: Evaluator, + systemProperties: Map[String, String]) extends ApplyHandler[Task] { + + systemProperties.foreach {case (k,v) => + System.setProperty(k,v) + } + // Evaluate classLoaderSig only once in the REPL to avoid busting caches // as the user enters more REPL commands and changes the classpath val classLoaderSig = Evaluator.classLoaderSig diff --git a/main/src/main/RunScript.scala b/main/src/main/RunScript.scala index ea8e554f..ab53aa1a 100644 --- a/main/src/main/RunScript.scala +++ b/main/src/main/RunScript.scala @@ -30,9 +30,14 @@ object RunScript{ stateCache: Option[Evaluator.State], log: Logger, env : Map[String, String], - keepGoing: Boolean) + keepGoing: Boolean, + systemProperties: Map[String, String]) : (Res[(Evaluator, Seq[PathRef], Either[String, Seq[ujson.Value]])], Seq[(os.Path, Long)]) = { + systemProperties.foreach {case (k,v) => + System.setProperty(k, v) + } + val (evalState, interpWatched) = stateCache match{ case Some(s) if watchedSigUnchanged(s.watched) => Res.Success(s) -> s.watched case _ => diff --git a/main/src/modules/Jvm.scala b/main/src/modules/Jvm.scala index 555fabae..9ee51aea 100644 --- a/main/src/modules/Jvm.scala +++ b/main/src/modules/Jvm.scala @@ -84,7 +84,7 @@ object Jvm { process.waitFor() if (process.exitCode() == 0) () - else throw new Exception("Interactive Subprocess Failed") + else throw new Exception("Interactive Subprocess Failed (exit code " + process.exitCode() + ")") } /** diff --git a/main/test/src/main/ClientServerTests.scala b/main/test/src/main/ClientServerTests.scala index 05238a5f..6d918b30 100644 --- a/main/test/src/main/ClientServerTests.scala +++ b/main/test/src/main/ClientServerTests.scala @@ -13,7 +13,8 @@ class EchoServer extends MillServerMain[Int]{ stdout: PrintStream, stderr: PrintStream, env: Map[String, String], - setIdle: Boolean => Unit) = { + setIdle: Boolean => Unit, + systemProperties: Map[String, String]) = { val reader = new BufferedReader(new InputStreamReader(stdin)) val str = reader.readLine() @@ -23,6 +24,9 @@ class EchoServer extends MillServerMain[Int]{ env.toSeq.sortBy(_._1).foreach{ case (key, value) => stdout.println(s"$key=$value") } + systemProperties.toSeq.sortBy(_._1).foreach{ + case (key, value) => stdout.println(s"$key=$value") + } stdout.flush() if (args.nonEmpty){ stderr.println(str.toUpperCase + args(0)) diff --git a/main/test/src/util/ScriptTestSuite.scala b/main/test/src/util/ScriptTestSuite.scala index 92f57c4f..b15541f3 100644 --- a/main/test/src/util/ScriptTestSuite.scala +++ b/main/test/src/util/ScriptTestSuite.scala @@ -16,10 +16,19 @@ abstract class ScriptTestSuite(fork: Boolean) extends TestSuite{ val disableTicker = false val debugLog = false val keepGoing = false + val systemProperties = Map[String, String]() lazy val runner = new mill.main.MainRunner( - ammonite.main.Cli.Config(wd = wd), disableTicker, - stdOutErr, stdOutErr, stdIn, None, Map.empty, - b => (), debugLog, keepGoing = keepGoing + config = ammonite.main.Cli.Config(wd = wd), + disableTicker = disableTicker, + outprintStream = stdOutErr, + errPrintStream = stdOutErr, + stdIn = stdIn, + stateCache0 = None, + env = Map.empty, + setIdle = b => (), + debugLog = debugLog, + keepGoing = keepGoing, + systemProperties = systemProperties ) def eval(s: String*) = { if (!fork) runner.runScript(workspacePath / buildPath , s.toList) diff --git a/main/test/src/util/TestEvaluator.scala b/main/test/src/util/TestEvaluator.scala index 97be20be..45bc41d9 100644 --- a/main/test/src/util/TestEvaluator.scala +++ b/main/test/src/util/TestEvaluator.scala @@ -12,12 +12,12 @@ object TestEvaluator{ val externalOutPath = os.pwd / 'target / 'external - def static(module: TestUtil.BaseModule)(implicit fullName: sourcecode.FullName) = { + def static(module: => TestUtil.BaseModule)(implicit fullName: sourcecode.FullName) = { new TestEvaluator(module)(fullName, TestPath(Nil)) } } -class TestEvaluator(module: TestUtil.BaseModule, failFast: Boolean = false) +class TestEvaluator(module: => TestUtil.BaseModule, failFast: Boolean = false) (implicit fullName: sourcecode.FullName, tp: TestPath){ val outPath = TestUtil.getOutPath() diff --git a/scalalib/resources/mill/scalalib/scalafmt/default.scalafmt.conf b/scalalib/resources/mill/scalalib/scalafmt/default.scalafmt.conf new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/scalalib/resources/mill/scalalib/scalafmt/default.scalafmt.conf diff --git a/scalalib/src/JavaModule.scala b/scalalib/src/JavaModule.scala index 75a0339d..e1fd8628 100644 --- a/scalalib/src/JavaModule.scala +++ b/scalalib/src/JavaModule.scala @@ -163,7 +163,6 @@ trait JavaModule extends mill.Module with TaskModule { outer => finalMainClassOpt().toOption match{ case None => "" case Some(cls) => - val isWin = scala.util.Properties.isWin mill.modules.Jvm.launcherUniversalScript( cls, Agg("$0"), Agg("%~dpnx0"), diff --git a/scalalib/src/scalafmt/ScalafmtModule.scala b/scalalib/src/scalafmt/ScalafmtModule.scala index 6a81d975..ea254e6d 100644 --- a/scalalib/src/scalafmt/ScalafmtModule.scala +++ b/scalalib/src/scalafmt/ScalafmtModule.scala @@ -11,23 +11,12 @@ trait ScalafmtModule extends JavaModule { .worker() .reformat( filesToFormat(sources()), - scalafmtConfig().head, - scalafmtDeps().map(_.path) + scalafmtConfig().head ) } - def scalafmtVersion: T[String] = "1.5.1" - def scalafmtConfig: Sources = T.sources(os.pwd / ".scalafmt.conf") - def scalafmtDeps: T[Agg[PathRef]] = T { - Lib.resolveDependencies( - zincWorker.repositories, - Lib.depToDependency(_, "2.12.4"), - Seq(ivy"com.geirsson::scalafmt-cli:${scalafmtVersion()}") - ) - } - protected def filesToFormat(sources: Seq[PathRef]) = { for { pathRef <- sources if os.exists(pathRef.path) @@ -46,8 +35,7 @@ object ScalafmtModule extends ExternalModule with ScalafmtModule { .worker() .reformat( files, - scalafmtConfig().head, - scalafmtDeps().map(_.path) + scalafmtConfig().head ) } diff --git a/scalalib/src/scalafmt/ScalafmtWorker.scala b/scalalib/src/scalafmt/ScalafmtWorker.scala index 47d8375f..f9c7e9b4 100644 --- a/scalalib/src/scalafmt/ScalafmtWorker.scala +++ b/scalalib/src/scalafmt/ScalafmtWorker.scala @@ -1,9 +1,11 @@ package mill.scalalib.scalafmt +import java.nio.file.{Paths => JPaths} + import mill._ import mill.define.{Discover, ExternalModule, Worker} -import mill.modules.Jvm import mill.api.Ctx +import org.scalafmt.interfaces.Scalafmt import scala.collection.mutable @@ -18,8 +20,7 @@ private[scalafmt] class ScalafmtWorker { private var configSig: Int = 0 def reformat(input: Seq[PathRef], - scalafmtConfig: PathRef, - scalafmtClasspath: Agg[os.Path])(implicit ctx: Ctx): Unit = { + scalafmtConfig: PathRef)(implicit ctx: Ctx): Unit = { val toFormat = if (scalafmtConfig.sig != configSig) input else @@ -28,8 +29,7 @@ private[scalafmt] class ScalafmtWorker { if (toFormat.nonEmpty) { ctx.log.info(s"Formatting ${toFormat.size} Scala sources") reformatAction(toFormat.map(_.path), - scalafmtConfig.path, - scalafmtClasspath) + scalafmtConfig.path) reformatted ++= toFormat.map { ref => val updRef = PathRef(ref.path) updRef.path -> updRef.sig @@ -43,15 +43,22 @@ private[scalafmt] class ScalafmtWorker { private val cliFlags = Seq("--non-interactive", "--quiet") private def reformatAction(toFormat: Seq[os.Path], - config: os.Path, - classpath: Agg[os.Path])(implicit ctx: Ctx) = { - val configFlags = - if (os.exists(config)) Seq("--config", config.toString) else Seq.empty - Jvm.runSubprocess( - "org.scalafmt.cli.Cli", - classpath, - mainArgs = toFormat.map(_.toString) ++ configFlags ++ cliFlags - ) - } + config: os.Path)(implicit ctx: Ctx) = { + val scalafmt = + Scalafmt + .create(this.getClass.getClassLoader) + .withRespectVersion(false) + + val configPath = + if (os.exists(config)) + config.toNIO + else + JPaths.get(getClass.getResource("default.scalafmt.conf").toURI) + toFormat.foreach { pathToFormat => + val code = os.read(pathToFormat) + val formatteCode = scalafmt.format(configPath, pathToFormat.toNIO, code) + os.write.over(pathToFormat, formatteCode) + } + } } diff --git a/scalalib/worker/src/ZincWorkerImpl.scala b/scalalib/worker/src/ZincWorkerImpl.scala index a127e96e..30f6b834 100644 --- a/scalalib/worker/src/ZincWorkerImpl.scala +++ b/scalalib/worker/src/ZincWorkerImpl.scala @@ -336,8 +336,7 @@ class ZincWorkerImpl(compilerBridge: Either[ pr = { val prev = store.get() PreviousResult.of(prev.map(_.getAnalysis), prev.map(_.getMiniSetup)) - }, - Optional.empty[java.io.File] + } ) try { |