From 7b4ced648ecd9b79b3a16d67552f0bb69f4dd543 Mon Sep 17 00:00:00 2001 From: David Gregory Date: Wed, 31 Oct 2018 20:44:53 +0000 Subject: Add tut contrib module (#464) * Add tut contrib module * Add TutModule tests and documentation * Use Path instead of PathRef for tut target directory * Use the correct scala version in TutModule * Ensure resolving tut doesn't bring in extra scala-library jars * Ensure MILL_VERSION system property is set in tut tests * Fork to run tut to fix classpath problems, add test with library usage * Follow convention w.r.t. publishVersion in testArgs * Add Scaladoc to TutModule * Don't supply a default version of Tut * Update docs to account for mandatory tutVersion setting * Inline tutArgs, otherwise Tut does not recompile when sources change --- build.sc | 6 +- contrib/tut/src/mill/contrib/tut/TutModule.scala | 132 +++++++++++++++++++++ .../tut/test/src/mill/contrib/tut/TutTests.scala | 124 +++++++++++++++++++ .../test/tut-with-libraries/TutWithLibraries.md | 10 ++ contrib/tut/test/tut/TutExample.md | 3 + docs/pages/9 - Contrib Modules.md | 56 +++++++++ main/core/src/mill/util/JsonFormatters.scala | 7 ++ 7 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 contrib/tut/src/mill/contrib/tut/TutModule.scala create mode 100644 contrib/tut/test/src/mill/contrib/tut/TutTests.scala create mode 100644 contrib/tut/test/tut-with-libraries/TutWithLibraries.md create mode 100644 contrib/tut/test/tut/TutExample.md diff --git a/build.sc b/build.sc index aa1cf428..878f5613 100755 --- a/build.sc +++ b/build.sc @@ -250,6 +250,10 @@ object contrib extends MillModule { } } + object tut extends MillModule { + def moduleDeps = Seq(scalalib) + def testArgs = Seq("-DMILL_VERSION=" + build.publishVersion()._2) + } } @@ -362,7 +366,7 @@ def launcherScript(shellJvmArgs: Seq[String], } object dev extends MillModule{ - def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, contrib.scalapblib) + def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, contrib.scalapblib, contrib.tut) def forkArgs = ( diff --git a/contrib/tut/src/mill/contrib/tut/TutModule.scala b/contrib/tut/src/mill/contrib/tut/TutModule.scala new file mode 100644 index 00000000..f051a465 --- /dev/null +++ b/contrib/tut/src/mill/contrib/tut/TutModule.scala @@ -0,0 +1,132 @@ +package mill +package contrib.tut + +import ammonite.ops._ +import coursier.MavenRepository +import mill.scalalib._ +import scala.util.matching.Regex + +/** + * 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. + * + * Extending this trait declares a Scala module which compiles markdown, HTML and `.txt` files in the `tut` folder of the module with Tut. + * + * By default the resulting documents are simply placed in the Mill build output folder but they can be placed elsewhere by overriding the [[mill.contrib.tut.TutModule#tutTargetDirectory]] task. + * + * For example: + * + * {{{ + * // build.sc + * import mill._, scalalib._, contrib.tut.__ + * + * object example extends TutModule { + * def scalaVersion = "2.12.6" + * def tutVersion = "0.6.7" + * } + * }}} + * + * This defines a project with the following layout: + * + * {{{ + * build.sc + * example/ + * src/ + * tut/ + * resources/ + * }}} + * + * In order to compile documentation we can execute the `tut` task in the module: + * + * {{{ + * sh> mill example.tut + * }}} + */ +trait TutModule extends ScalaModule { + /** + * 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. + */ + def tutSourceDirectory = T.sources { millSourcePath / 'tut } + + /** + * A task which determines where the compiled documentation files will be placed. By default this is simply the Mill build's output folder for this task, + * but this can be reconfigured so that documentation goes to the root of the module (e.g. `millSourcePath`) or to a dedicated folder (e.g. `millSourcePath / 'docs`) + */ + def tutTargetDirectory: T[Path] = T { T.ctx().dest } + + /** + * A task which determines what classpath is used when compiling documentation. By default this is configured to use the same inputs as the [[mill.contrib.tut.TutModule#runClasspath]], + * except for using [[mill.contrib.tut.TutModule#tutIvyDeps]] rather than the module's [[mill.contrib.tut.TutModule#runIvyDeps]]. + */ + def tutClasspath: T[Agg[PathRef]] = T { + // Same as runClasspath but with tut added to ivyDeps from the start + // This prevents duplicate, differently versioned copies of scala-library ending up on the classpath which can happen when resolving separately + transitiveLocalClasspath() ++ + resources() ++ + localClasspath() ++ + unmanagedClasspath() ++ + tutIvyDeps() + } + + /** + * A task which determines the scalac plugins which will be used when compiling code examples with Tut. The default is to use the [[mill.contrib.tut.TutModule#scalacPluginIvyDeps]] for the module. + */ + def tutScalacPluginIvyDeps: T[Agg[Dep]] = scalacPluginIvyDeps() + + /** + * A [[scala.util.matching.Regex]] task which will be used to determine which files should be compiled with tut. The default pattern is as follows: `.*\.(md|markdown|txt|htm|html)`. + */ + def tutNameFilter: T[Regex] = T { """.*\.(md|markdown|txt|htm|html)""".r } + + /** + * The scalac options which will be used when compiling code examples with Tut. The default is to use the [[mill.contrib.tut.TutModule#scalacOptions]] for the module, + * but filtering out options which are problematic in the REPL, e.g. `-Xfatal-warnings`, `-Ywarn-unused-imports`. + */ + def tutScalacOptions: T[Seq[String]] = + scalacOptions().filterNot(Set( + "-Ywarn-unused:imports", + "-Ywarn-unused-import", + "-Ywarn-dead-code", + "-Xfatal-warnings" + )) + + /** + * The version of Tut to use. + */ + def tutVersion: T[String] + + /** + * A task which determines how to fetch the Tut jar file and all of the dependencies required to compile documentation for the module and returns the resulting files. + */ + def tutIvyDeps: T[Agg[PathRef]] = T { + Lib.resolveDependencies( + repositories :+ MavenRepository(s"https://dl.bintray.com/tpolecat/maven"), + Lib.depToDependency(_, scalaVersion()), + compileIvyDeps() ++ transitiveIvyDeps() ++ Seq( + ivy"org.tpolecat::tut-core:${tutVersion()}" + ) + ) + } + + /** + * A task which performs the dependency resolution for the scalac plugins to be used with Tut. + */ + def tutPluginJars: T[Agg[PathRef]] = resolveDeps(tutScalacPluginIvyDeps)() + + /** + * Run Tut using the configuration specified in this module. The working directory used is the [[mill.contrib.tut.TutModule#millSourcePath]]. + */ + def tut: T[CommandResult] = T { + val in = tutSourceDirectory().head.path.toIO.getAbsolutePath + val out = tutTargetDirectory().toIO.getAbsolutePath + val re = tutNameFilter() + val opts = tutScalacOptions() + val pOpts = tutPluginJars().map(pathRef => "-Xplugin:" + pathRef.path.toIO.getAbsolutePath) + val tutArgs = List(in, out, re.pattern.toString) ++ opts ++ pOpts + %%( + 'java, + "-cp", tutClasspath().map(_.path.toIO.getAbsolutePath).mkString(java.io.File.pathSeparator), + "tut.TutMain", + tutArgs + )(wd = millSourcePath) + } +} diff --git a/contrib/tut/test/src/mill/contrib/tut/TutTests.scala b/contrib/tut/test/src/mill/contrib/tut/TutTests.scala new file mode 100644 index 00000000..fd369eed --- /dev/null +++ b/contrib/tut/test/src/mill/contrib/tut/TutTests.scala @@ -0,0 +1,124 @@ +package mill.contrib +package tut + +import ammonite.ops._ +import mill._ +import mill.eval.Result._ +import mill.scalalib._ +import mill.util.{TestEvaluator, TestUtil} +import utest._ +import utest.framework.TestPath + +object TutTests extends TestSuite { + + trait TutTestModule extends TestUtil.BaseModule with TutModule { + def millSourcePath = TestUtil.getSrcPathBase() / millOuterCtx.enclosing.split('.') + def scalaVersion = "2.12.4" + def tutVersion = "0.6.7" + } + + object TutTest extends TutTestModule + + object TutCustomTest extends TutTestModule { + def tutTargetDirectory = millSourcePath + } + + object TutLibrariesTest extends TutTestModule { + def ivyDeps = Agg(ivy"org.typelevel::cats-core:1.4.0") + def tutSourceDirectory = T.sources { resourcePathWithLibraries } + def scalacPluginIvyDeps = Agg(ivy"org.spire-math::kind-projector:0.9.8") + } + + val resourcePath = pwd / 'contrib / 'tut / 'test / 'tut + val resourcePathWithLibraries = pwd / 'contrib / 'tut / 'test / "tut-with-libraries" + + def workspaceTest[T](m: TestUtil.BaseModule, resourcePath: Path = resourcePath) + (t: TestEvaluator => T) + (implicit tp: TestPath): T = { + val eval = new TestEvaluator(m) + rm(m.millSourcePath) + rm(eval.outPath) + mkdir(m.millSourcePath) + cp(resourcePath, m.millSourcePath / 'tut) + t(eval) + } + + def tests: Tests = Tests { + 'tut - { + 'createOutputFile - workspaceTest(TutTest) { eval => + val expectedPath = + eval.outPath / 'tutTargetDirectory / 'dest / "TutExample.md" + + val expected = + """ + |```scala + |scala> 1 + 1 + |res0: Int = 2 + |``` + | + """.trim.stripMargin + + val Right((result, evalCount)) = eval.apply(TutTest.tut) + + assert( + exists(expectedPath) && + read! expectedPath == expected + ) + } + + 'supportCustomSettings - workspaceTest(TutCustomTest) { eval => + val defaultPath = + eval.outPath / 'tutTargetDirectory / 'dest / "TutExample.md" + val expectedPath = + TutCustomTest.millSourcePath / "TutExample.md" + + val expected = + """ + |```scala + |scala> 1 + 1 + |res0: Int = 2 + |``` + | + """.trim.stripMargin + + val Right((result, evalCount)) = eval.apply(TutCustomTest.tut) + + assert( + !exists(defaultPath) && + exists(expectedPath) && + read! expectedPath == expected + ) + } + + 'supportUsingLibraries - workspaceTest(TutLibrariesTest, resourcePath = resourcePathWithLibraries) { eval => + val expectedPath = + eval.outPath / 'tutTargetDirectory / 'dest / "TutWithLibraries.md" + + val expected = + """ + |```scala + |import cats._ + |import cats.arrow.FunctionK + |import cats.implicits._ + |``` + | + |```scala + |scala> List(1, 2, 3).combineAll + |res0: Int = 6 + | + |scala> λ[FunctionK[List, Option]](_.headOption)(List(1, 2 ,3)) + |res1: Option[Int] = Some(1) + |``` + | + """.trim.stripMargin + + val Right(_) = eval.apply(TutLibrariesTest.tut) + + assert( + exists(expectedPath) && + read! expectedPath == expected + ) + } + } + } +} diff --git a/contrib/tut/test/tut-with-libraries/TutWithLibraries.md b/contrib/tut/test/tut-with-libraries/TutWithLibraries.md new file mode 100644 index 00000000..7df1e703 --- /dev/null +++ b/contrib/tut/test/tut-with-libraries/TutWithLibraries.md @@ -0,0 +1,10 @@ +```tut:silent +import cats._ +import cats.arrow.FunctionK +import cats.implicits._ +``` + +```tut +List(1, 2, 3).combineAll +λ[FunctionK[List, Option]](_.headOption)(List(1, 2 ,3)) +``` diff --git a/contrib/tut/test/tut/TutExample.md b/contrib/tut/test/tut/TutExample.md new file mode 100644 index 00000000..c149fbc6 --- /dev/null +++ b/contrib/tut/test/tut/TutExample.md @@ -0,0 +1,3 @@ +```tut +1 + 1 +``` diff --git a/docs/pages/9 - Contrib Modules.md b/docs/pages/9 - Contrib Modules.md index 4ddf097a..cd02f2c2 100644 --- a/docs/pages/9 - Contrib Modules.md +++ b/docs/pages/9 - Contrib Modules.md @@ -95,6 +95,62 @@ object project extends ScalaModule { } ``` +### 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. + +To declare a module that uses Tut you can extend the `mill.contrib.tut.TutModule` trait when defining your module. + +This creates a Scala module which compiles markdown, HTML and `.txt` files in the `tut` folder of the module with Tut. + +By default the resulting documents are simply placed in the Mill build output folder but they can be placed elsewhere by overriding the `tutTargetDirectory` task. + +```scala +// build.sc +import mill._, scalalib._, contrib.tut.__ + +object example extends TutModule { + def scalaVersion = "2.12.6" + def tutVersion = "0.6.7" +} +``` + +This defines a project with the following layout: + +``` +build.sc +example/ + src/ + tut/ + resources/ +``` + +In order to compile documentation we can execute the `tut` task in the module: + +``` +sh> mill example.tut +``` + +#### 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. + +* tutTargetDirectory - A task which determines where the compiled documentation files will be placed. By default this is simply the Mill build's output folder for the `tutTargetDirectory` task but this can be reconfigured so that documentation goes to the root of the module (e.g. `millSourcePath`) or to a dedicated folder (e.g. `millSourcePath / 'docs`) + +* tutClasspath - A task which determines what classpath is used when compiling documentation. By default this is configured to use the same inputs as the `runClasspath`, except for using `tutIvyDeps` rather than the module's `ivyDeps`. + +* tutScalacPluginIvyDeps - A task which determines the scalac plugins which will be used when compiling code examples with Tut. The default is to use the `scalacPluginIvyDeps` for the module. + +* tutNameFilter - A `scala.util.matching.Regex` task which will be used to determine which files should be compiled with tut. The default pattern is as follows: `.*\.(md|markdown|txt|htm|html)`. + +* tutScalacOptions - The scalac options which will be used when compiling code examples with Tut. The default is to use the `scalacOptions` for the module but filtering out options which are problematic in the REPL, e.g. `-Xfatal-warnings`, `-Ywarn-unused-imports`. + +* tutVersion - The version of Tut to use. + +* tutIvyDeps - A task which determines how to fetch the Tut jar file and all of the dependencies required to compile documentation for the module and returns the resulting files. + +* tutPluginJars - A task which performs the dependency resolution for the scalac plugins to be used with Tut. + ### Twirl Twirl templates support. diff --git a/main/core/src/mill/util/JsonFormatters.scala b/main/core/src/mill/util/JsonFormatters.scala index f92941f7..2728d94d 100644 --- a/main/core/src/mill/util/JsonFormatters.scala +++ b/main/core/src/mill/util/JsonFormatters.scala @@ -3,6 +3,7 @@ package mill.util import ammonite.ops.{Bytes, Path} import upickle.Js import upickle.default.{ReadWriter => RW} +import scala.util.matching.Regex object JsonFormatters extends JsonFormatters trait JsonFormatters { implicit val pathReadWrite: RW[ammonite.ops.Path] = upickle.default.readwriter[String] @@ -11,6 +12,12 @@ trait JsonFormatters { Path(_) ) + implicit val regexReadWrite: RW[Regex] = upickle.default.readwriter[String] + .bimap[Regex]( + _.pattern.toString, + _.r + ) + implicit val bytesReadWrite: RW[Bytes] = upickle.default.readwriter[String] .bimap( o => javax.xml.bind.DatatypeConverter.printBase64Binary(o.array), -- cgit v1.2.3