Skip to content

Commit 39e353b

Browse files
authored
Improve example zips (#6122)
- Make `ExampleParser` return proper `enum` cases, rather than stringly-typed tuples - For examples where `build.mill` doesn't contain any actual code or YAML header config, e.g. the script examples where it's just for docs and usage blocks, remove the `build.mill` and render a `readme.adoc` instead - For examples where the `build.mill` does contain code or header config, use the `testRepoSourceRoot` containing any `build.mill` templates expanded, rather than the raw `moduleDir`. Tested manually via ` ./mill show dist.exampleZips`
1 parent 1f82de9 commit 39e353b

File tree

4 files changed

+93
-34
lines changed

4 files changed

+93
-34
lines changed

dist/package.mill

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import scalalib.*
55
import publish.*
66
import mill.api.ModuleRef
77
import mill.util.Jvm
8+
import mill.testkit.Chunk
89
//import de.tobiasroeser.mill.vcs.version.VcsVersion
910
import millbuild.*
1011
import mill.api.BuildCtx
@@ -268,9 +269,14 @@ object `package` extends MillJavaModule with DistModule {
268269
build.example.exampleModules.map(_.moduleDir)
269270
}
270271

271-
def examplePathRefs: Task[Seq[PathRef]] = Task.Sources(
272-
build.example.exampleModules.map(_.moduleDir)*
273-
)
272+
def examplePathRefs: Task[Seq[PathRef]] =
273+
Task.traverse(build.example.exampleModules)(_.testRepoSourceRoot)
274+
275+
def exampleRendered: Task[Seq[PathRef]] =
276+
Task.traverse(build.example.exampleModules)(_.rendered)
277+
278+
def exampleParsed: Task[Seq[Seq[Chunk]]] =
279+
Task.traverse(build.example.exampleModules)(_.parsed)
274280

275281
def exampleArtifactNames = Task {
276282
for (path <- examplePaths()) yield {
@@ -281,9 +287,17 @@ object `package` extends MillJavaModule with DistModule {
281287
}
282288

283289
def exampleZips: T[Seq[PathRef]] = Task {
284-
examplePathRefs().zip(exampleArtifactNames()).map {
285-
case (pr, exampleStr) =>
290+
examplePathRefs().zip(exampleArtifactNames()).zip(exampleRendered()).zip(exampleParsed()).map {
291+
case (((pr, exampleStr), rendered), parsed) =>
286292
os.copy(pr.path, Task.dest / exampleStr, createFolders = true)
293+
294+
// When the `build.mill` contains no Scala or Yaml chunks, it purely serves
295+
// as the doc-site content and isn't used in the build. So we don't need to include it
296+
// and cna instead just render the readme.adoc file for people to read
297+
if (!parsed.exists(c => c.isInstanceOf[Chunk.Scala] || c.isInstanceOf[Chunk.Yaml])) {
298+
os.remove.all(Task.dest / exampleStr / "build.mill")
299+
os.copy(rendered.path, Task.dest / exampleStr / "readme.adoc")
300+
}
287301
val ignoreErrorsOnCI = Task.dest / exampleStr / "ignoreErrorsOnCI"
288302
if (os.exists(ignoreErrorsOnCI)) os.remove(ignoreErrorsOnCI)
289303
val buildMill = Task.dest / exampleStr / "build.mill"

example/package.mill

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import mill.contrib.buildinfo.BuildInfo
1313
import mill.T
1414
import mill.api.Cross
1515
import mill.api.BuildCtx
16+
import mill.testkit.Chunk
1617

1718
object `package` extends Module {
1819
def exampleModules: Seq[ExampleCrossModule] = moduleInternal
@@ -246,25 +247,26 @@ object `package` extends Module {
246247
/**
247248
* Parses a `build.mill` for specific comments and return the split-by-type content
248249
*/
249-
def parsed: T[Seq[(String, String)]] = Task {
250+
def parsed: T[Seq[mill.testkit.Chunk]] = Task {
250251
mill.testkit.ExampleParser(testRepoSourceRoot().path)
251252
}
252253

253254
def rendered = Task {
254255
var seenCode = false
255256
var seenFrontMatter = false
256257
val examplePath = moduleDir.subRelativeTo(BuildCtx.workspaceRoot)
257-
val frontMatter = parsed().takeWhile(_._1 == "yaml").map(_._2).mkString("\n")
258-
val withoutFrontMatter = parsed().dropWhile(_._1 == "yaml")
258+
val frontMatter = parsed().takeWhile(_.isInstanceOf[Chunk.Yaml]).map {
259+
case Chunk.Yaml(lines) => lines.mkString("\n")
260+
}.mkString("\n")
261+
val withoutFrontMatter = parsed().dropWhile(_.isInstanceOf[Chunk.Yaml])
259262
val exampleDashed = examplePath.segments.mkString("-")
260263
val download =
261264
s"{mill-download-url}/mill-dist-${build.millVersion()}-$exampleDashed.zip[download]"
262265
os.write(
263266
Task.dest / "example.adoc",
264267
withoutFrontMatter
265-
.filter(_._2.nonEmpty)
266268
.map {
267-
case (s"see:$path", txt) =>
269+
case Chunk.See(path, lines) =>
268270
// avoid .stripMargin, as the embedded content may contain the margin symbol
269271
val lang = os.FilePath(path).ext match {
270272
case "mill" => "scala"
@@ -274,9 +276,9 @@ object `package` extends Module {
274276
.$path ($download, {mill-example-url}/$examplePath/$path[browse])
275277
[source,$lang,subs="attributes,verbatim"]
276278
----
277-
$txt
279+
${lines.mkString("\n")}
278280
----"""
279-
case ("scala", txt) =>
281+
case Chunk.Scala(lines) =>
280282
val title =
281283
if (seenCode) ""
282284
else {
@@ -297,19 +299,18 @@ ${
297299
}
298300
}
299301

300-
$txt
302+
${lines.mkString("\n")}
301303
----
302304
"""
303-
case ("comment", txt) => txt + "\n"
304-
case ("example", txt) =>
305+
case Chunk.Comment(lines) => lines.mkString("\n") + "\n"
306+
case Chunk.Example(lines) =>
305307
// avoid .stripMargin, as the embedded content may contain the margin symbol
306308
s"""
307309
[source,console,subs="attributes,verbatim"]
308310
----
309-
$txt
311+
${lines.mkString("\n")}
310312
----"""
311-
case (kind, txt) =>
312-
throw Exception(s"Unknown kind: $kind\n\nText: $txt")
313+
case Chunk.Yaml(_) => ""
313314
}
314315
.mkString("\n")
315316
)
Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
package mill.testkit
22

3+
enum Chunk derives upickle.default.ReadWriter {
4+
case Yaml(lines: Seq[String])
5+
case Example(lines: Seq[String])
6+
case See(path: String, lines: Seq[String])
7+
case Scala(lines: Seq[String])
8+
case Comment(lines: Seq[String])
9+
}
10+
311
object ExampleParser {
4-
def apply(testRepoRoot: os.Path): Seq[(String, String)] = {
12+
def apply(testRepoRoot: os.Path): Seq[Chunk] = {
13+
val result = collection.mutable.Buffer[Chunk]()
514

6-
val states = collection.mutable.Buffer("yaml")
7-
val chunks = collection.mutable.Buffer(collection.mutable.Buffer.empty[String])
15+
def appendLine(line: String): Unit = {
16+
if (result.nonEmpty) {
17+
val last = result.last
18+
val updated = last match {
19+
case Chunk.Yaml(lines) => Chunk.Yaml(lines :+ line)
20+
case Chunk.Example(lines) => Chunk.Example(lines :+ line)
21+
case Chunk.See(path, lines) => Chunk.See(path, lines :+ line)
22+
case Chunk.Scala(lines) => Chunk.Scala(lines :+ line)
23+
case Chunk.Comment(lines) => Chunk.Comment(lines :+ line)
24+
}
25+
result(result.length - 1) = updated
26+
}
27+
}
828

929
val rootBuildFileNames = Seq("build.mill")
1030
val buildFile = rootBuildFileNames.map(testRepoRoot / _)
@@ -14,26 +34,47 @@ object ExampleParser {
1434
s"No build file named ${rootBuildFileNames.mkString("/")} found in $testRepoRoot"
1535
)
1636
)
37+
1738
for (line <- os.read.lines(buildFile)) {
18-
val (newState, restOpt) = line match {
19-
case s"/** Usage" => ("example", None)
39+
val (newChunkType, lineToAdd) = line match {
40+
case s"/** Usage" => (Chunk.Example(Vector()), None)
2041
case s"/** See Also: $path */" =>
21-
(s"see:$path", Some(os.read(os.Path(path, testRepoRoot))))
22-
case s"*/" => ("scala", None)
23-
case line @ s"//|$_" if states.last == "yaml" => ("yaml", Some(line))
24-
case s"//$rest" if !rest.startsWith("|") => ("comment", Some(rest.stripPrefix(" ")))
42+
(Chunk.See(path, Vector()), Some(os.read(os.Path(path, testRepoRoot))))
43+
case s"*/" => (Chunk.Scala(Vector()), None)
44+
case line @ s"//|$_" if result.nonEmpty && result.last.isInstanceOf[Chunk.Yaml] =>
45+
(Chunk.Yaml(Vector()), Some(line))
46+
case s"//$rest" if !rest.startsWith("|") =>
47+
(Chunk.Comment(Vector()), Some(rest.stripPrefix(" ")))
2548
case l =>
26-
(if (Seq("comment", "yaml").contains(states.last)) "scala" else states.last, Some(l))
49+
val chunkType = if (result.nonEmpty) {
50+
result.last match {
51+
case _: Chunk.Comment | _: Chunk.Yaml => Chunk.Scala(Vector())
52+
case other => other
53+
}
54+
} else Chunk.Yaml(Vector()) // initial state
55+
(chunkType, Some(l))
2756
}
2857

29-
if (newState != states.last) {
30-
states.append(newState)
31-
chunks.append(collection.mutable.Buffer.empty[String])
58+
if (result.isEmpty || newChunkType.getClass != result.last.getClass) {
59+
result.append(newChunkType)
3260
}
3361

34-
restOpt.foreach(r => chunks.last.append(r))
62+
lineToAdd.foreach(appendLine)
3563
}
3664

37-
states.zip(chunks.map(_.mkString("\n").trim)).filter(_._2.nonEmpty).toSeq
65+
result.filter {
66+
case Chunk.Yaml(lines) => lines.mkString("\n").trim.nonEmpty
67+
case Chunk.Example(lines) => lines.mkString("\n").trim.nonEmpty
68+
case Chunk.See(_, lines) => lines.mkString("\n").trim.nonEmpty
69+
case Chunk.Scala(lines) => lines.mkString("\n").trim.nonEmpty
70+
case Chunk.Comment(lines) => lines.mkString("\n").trim.nonEmpty
71+
}.map {
72+
case Chunk.Yaml(lines) => Chunk.Yaml(lines.mkString("\n").trim.linesIterator.toVector)
73+
case Chunk.Example(lines) => Chunk.Example(lines.mkString("\n").trim.linesIterator.toVector)
74+
case Chunk.See(path, lines) =>
75+
Chunk.See(path, lines.mkString("\n").trim.linesIterator.toVector)
76+
case Chunk.Scala(lines) => Chunk.Scala(lines.mkString("\n").trim.linesIterator.toVector)
77+
case Chunk.Comment(lines) => Chunk.Comment(lines.mkString("\n").trim.linesIterator.toVector)
78+
}.toSeq
3879
}
3980
}

testkit/src/mill/testkit/ExampleTester.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mill.testkit
22

33
import mill.constants.Util.isWindows
4+
import mill.testkit.Chunk
45
import utest.*
56

67
import scala.util.control.NonFatal
@@ -236,7 +237,9 @@ class ExampleTester(
236237
val parsed = ExampleParser(workspaceSourcePath)
237238
val ignoreErrors = System.getenv("CI") != null &&
238239
os.exists(workspaceSourcePath / "ignoreErrorsOnCI")
239-
val usageComment = parsed.collect { case ("example", txt) => txt }.mkString("\n\n")
240+
val usageComment = parsed.collect { case Chunk.Example(lines) =>
241+
lines.mkString("\n")
242+
}.mkString("\n\n")
240243
val commandBlocks = ("\n" + usageComment.trim).split("\n> ").filter(_.nonEmpty)
241244

242245
try {

0 commit comments

Comments
 (0)