Skip to content

Commit

Permalink
Use mill examples in init (#3583)
Browse files Browse the repository at this point in the history
implements #3548 
Modifying init to fetch examples from releases page instead of using g8
template

This introduces new top-level module `initmodule`
this module generates resource `exampleList.txt` containing json with
array of pairs `(exampleId,exampleUrl)`
`exampleId` and `exampleUrl` are formed based on already present
`exampleZips` target, and `millVersion()`.

initmodule introduces external module `MillInitModule`
calls to` mill init ` are redirected to `MillInitModule.init` (basically
the same logic as it was with `Giter8Module.init`)

`MillInitModule.init` called without parameters prints message
containing `exampleids` list based on generated exampleList.txt in the
form of:

```
Run init with one of the following examples as an argument to download and extract example:
depth/cross/1-simple
depth/cross/2-cross-source-path
...
```
When called with a parameter it validates if passed parameter is in the
list, and if so, it downloads example from github releases page and
unpacks it into the directory from where the command was run.

---------

Co-authored-by: Li Haoyi <[email protected]>
  • Loading branch information
pawelsadlo and lihaoyi authored Sep 30, 2024
1 parent 05bef7e commit 5e51a3b
Show file tree
Hide file tree
Showing 13 changed files with 407 additions and 94 deletions.
19 changes: 14 additions & 5 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import mill.resolve.SelectMode
import mill.T
import mill.define.Cross

import scala.util.matching.Regex

// plugins and dependencies
import $meta._
import $file.ci.shared
Expand Down Expand Up @@ -743,7 +745,7 @@ object dist0 extends MillPublishJavaModule {

object dist extends MillPublishJavaModule {
def jar = rawAssembly()
def moduleDeps = Seq(build.runner, idea)
def moduleDeps = Seq(build.runner, idea, build.main.init)

def testTransitiveDeps = dist0.testTransitiveDeps() ++ Seq(
(s"com.lihaoyi-${dist.artifactId()}", dist0.runClasspath().map(_.path).mkString("\n"))
Expand Down Expand Up @@ -929,13 +931,20 @@ def bootstrapLauncher = T {
PathRef(outputPath)
}

def exampleZips: T[Seq[PathRef]] = T {
def examplePathsWithArtifactName:Task[Seq[(os.Path,String)]] = T.task{
for {
exampleMod <- build.example.exampleModules
examplePath = exampleMod.millSourcePath
path = exampleMod.millSourcePath
} yield {
val example = examplePath.subRelativeTo(T.workspace)
val exampleStr = millVersion() + "-" + example.segments.mkString("-")
val example = path.subRelativeTo(T.workspace)
val artifactName = millVersion() + "-" + example.segments.mkString("-")
(path, artifactName)
}
}


def exampleZips: T[Seq[PathRef]] = T{
examplePathsWithArtifactName().map{ case (examplePath, exampleStr) =>
os.copy(examplePath, T.dest / exampleStr, createFolders = true)
os.write(T.dest / exampleStr / ".mill-version", millLastTag())
os.copy(bootstrapLauncher().path, T.dest / exampleStr / "mill")
Expand Down
19 changes: 0 additions & 19 deletions docs/modules/ROOT/pages/Scala_Builtin_Commands.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,3 @@


include::example/scalalib/basic/4-builtin-commands.adoc[]

== init

[source,bash]
----
> mill -i init com-lihaoyi/mill-scala-hello.g8
....
A minimal Scala project.
name [Scala Seed Project]: hello
Template applied in ./hello
----

The `init` command generates a project based on a Giter8 template.
It prompts you to enter project name and creates a folder with that name.
You can use it to quickly generate a starter project.
There are lots of templates out there for many frameworks and tools!

37 changes: 37 additions & 0 deletions example/scalalib/basic/4-builtin-commands/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,40 @@ foo.compileClasspath
// Come by our https://discord.gg/MNAXQMAr[Discord Channel]
// if you want to ask questions or say hi!
//
//
// == init

/** Usage
> mill init
Run `mill init <example-id>` with one of these examples as an argument to download and extract example.
Run `mill init --show-all` to see full list of examples.
Run `mill init <Giter8 template>` to generate project from Giter8 template.
...
scalalib/basic/1-simple
...
scalalib/web/1-todo-webapp
scalalib/web/2-webapp-cache-busting
scalalib/web/3-scalajs-module
scalalib/web/4-webapp-scalajs
scalalib/web/5-webapp-scalajs-shared
...
javalib/basic/1-simple
...
javalib/builds/4-realistic
...
javalib/web/1-hello-jetty
javalib/web/2-hello-spring-boot
javalib/web/3-todo-spring-boot
javalib/web/4-hello-micronaut
javalib/web/5-todo-micronaut
kotlinlib/basic/1-simple
...
kotlinlib/builds/4-realistic
...
kotlinlib/web/1-hello-ktor
*/

// The `init` command generates a project based on a Mill example project or
// a Giter8 template. You can use it to quickly generate a starter project.
// There are lots of templates out there for many frameworks and tools!

16 changes: 16 additions & 0 deletions integration/feature/init/src/MillInitTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ object MillInitTests extends UtestIntegrationTestSuite {

def tests: Tests = Tests {
test("Mill init works") - integrationTest { tester =>
import tester._
val msg =
"""Run `mill init <example-id>` with one of these examples as an argument to download and extract example.
|Run `mill init --show-all` to see full list of examples.
|Run `mill init <Giter8 template>` to generate project from Giter8 template.""".stripMargin
val res = eval("init")
res.isSuccess ==> true

val exampleListOut = out("init")
val parsed = exampleListOut.json.arr.map(_.str)
assert(parsed.nonEmpty)
assert(res.out.startsWith(msg))
assert(res.out.endsWith(msg))
}

test("Mill init works for g8 templates") - integrationTest { tester =>
import tester._
eval(("init", "com-lihaoyi/mill-scala-hello.g8", "--name=example")).isSuccess ==> true
val projFile = workspacePath / "example/build.sc"
Expand Down
7 changes: 4 additions & 3 deletions kotlinlib/src/mill/kotlinlib/contrib/kover/KoverModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ trait KoverModule extends KotlinModule { outer =>
Success[String](T.env.getOrElse("KOVER_VERSION", Versions.koverVersion))
}

def koverBinaryReport: T[PathRef] =
Task.Persistent { PathRef(koverDataDir().path / "kover-report.ic") }
def koverBinaryReport: T[PathRef] = Task(persistent = true) {
PathRef(koverDataDir().path / "kover-report.ic")
}

def koverDataDir: T[PathRef] = Task.Persistent { PathRef(T.dest) }
def koverDataDir: T[PathRef] = Task(persistent = true) { PathRef(T.dest) }

object kover extends Module with KoverReportBaseModule {

Expand Down
55 changes: 55 additions & 0 deletions main/codesig/package.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package build.main.codesig
import mill._, scalalib._

object `package` extends RootModule with build.MillPublishScalaModule {
override def ivyDeps = Agg(build.Deps.asmTree, build.Deps.osLib, build.Deps.pprint)
def moduleDeps = Seq(build.main.util)

override lazy val test: CodeSigTests = new CodeSigTests {}
trait CodeSigTests extends MillScalaTests {
val caseKeys = build.interp.watchValue(
os.walk(millSourcePath / "cases", maxDepth = 3)
.map(_.subRelativeTo(millSourcePath / "cases").segments)
.collect { case Seq(a, b, c) => s"$a-$b-$c" }
)

def testLogFolder = T { T.dest }

def caseEnvs[V](f1: CaseModule => Task[V])(s: String, f2: V => String) = {
T.traverse(caseKeys) { i => f1(cases(i)).map(v => s"MILL_TEST_${s}_$i" -> f2(v)) }
}
def forkEnv = T {
Map("MILL_TEST_LOGS" -> testLogFolder().toString) ++
caseEnvs(_.compile)("CLASSES", _.classes.path.toString)() ++
caseEnvs(_.compileClasspath)("CLASSPATH", _.map(_.path).mkString(","))() ++
caseEnvs(_.sources)("SOURCES", _.head.path.toString)()
}

object cases extends Cross[CaseModule](caseKeys)
trait CaseModule extends ScalaModule with Cross.Module[String] {
def caseName = crossValue
object external extends ScalaModule {
def scalaVersion = build.Deps.scalaVersion
}

def moduleDeps = Seq(external)

val Array(prefix, suffix, rest) = caseName.split("-", 3)
def millSourcePath = super.millSourcePath / prefix / suffix / rest
def scalaVersion = build.Deps.scalaVersion
def ivyDeps = T {
if (!caseName.contains("realistic") && !caseName.contains("sourcecode")) super.ivyDeps()
else Agg(
build.Deps.fastparse,
build.Deps.scalatags,
build.Deps.cask,
build.Deps.castor,
build.Deps.mainargs,
build.Deps.requests,
build.Deps.osLib,
build.Deps.upickle
)
}
}
}
}
67 changes: 67 additions & 0 deletions main/init/package.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package build.main.init

import mill._
import scala.util.matching.Regex

object `package` extends RootModule with build.MillPublishScalaModule {
def moduleDeps = Seq(build.main)

override def resources = T.sources {
super.resources() ++ Seq(exampleList())
}

def exampleList: T[PathRef] = T {

val versionPattern: Regex = "\\d+\\.\\d+\\.\\d+".r
val rcPattern: Regex = "-RC\\d+".r

val millVer: String = build.millVersion()
val lastTag = versionPattern.findFirstMatchIn(millVer) match {
case Some(verMatch) =>
val maybeRC = rcPattern.findFirstIn(verMatch.after)
verMatch.matched + maybeRC.getOrElse("")
case None => "0.0.0"
}

val data: Seq[(os.SubPath, String)] = build.examplePathsWithArtifactName().map { case (path, str) =>
val downloadUrl = build.Settings.projectUrl + "/releases/download/" + lastTag + "/" + str + ".zip"
val subPath = path.subRelativeTo(T.workspace / "example")
(subPath, downloadUrl)
}

val libsSortOrder = List("scalalib", "javalib", "kotlinlib", "extending", "external", "thirdparty", "depth")
val categoriesSortOrder = List("basic", "builds", "web")

def sortCriterium(strOpt: Option[String], sortOrderList: List[String]): Int =
strOpt.flatMap { str =>
val idx = sortOrderList.indexOf(str)
Option.when(idx >= 0)(idx)
}.getOrElse(Int.MaxValue)

val sortedData = data.sortBy { case (p1, _) =>
val segmentsReversed = p1.segments.reverse.lift
val libOpt = segmentsReversed(2)
val categoryOpt = segmentsReversed(1)
val nameOpt = segmentsReversed(0)

val libSortCriterium = sortCriterium(libOpt, libsSortOrder)
val categorySortCriterium = sortCriterium(categoryOpt, categoriesSortOrder)
val nameSortCriterium = nameOpt.flatMap(_.takeWhile(_.isDigit).toIntOption).getOrElse(Int.MinValue)
(libSortCriterium, libOpt, categorySortCriterium, categoryOpt, nameSortCriterium, nameOpt)
}

val stream = os.write.outputStream(T.dest / "exampleList.txt")
val writer = new java.io.OutputStreamWriter(stream)
try {
val json = upickle.default.writeTo(sortedData.map { case (p, s) => (p.toString(), s) }, writer)
PathRef(T.dest)
} catch {
case ex: Throwable => T.log.error(ex.toString); throw ex
} finally {
writer.close()
stream.close()
}

}

}
99 changes: 99 additions & 0 deletions main/init/src/mill/init/InitModule.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package mill.init

import mainargs.{Flag, arg}
import mill.api.IO
import mill.define.{Discover, ExternalModule}
import mill.util.Util.download
import mill.{Command, Module, T}

import java.io.IOException
import java.util.UUID
import scala.util.{Failure, Success, Try, Using}

object InitModule extends ExternalModule with InitModule {
lazy val millDiscover: Discover = Discover[this.type]
}

trait InitModule extends Module {

type ExampleUrl = String
type ExampleId = String

val msg: String =
"""Run `mill init <example-id>` with one of these examples as an argument to download and extract example.
|Run `mill init --show-all` to see full list of examples.
|Run `mill init <Giter8 template>` to generate project from Giter8 template.""".stripMargin
def moduleNotExistMsg(id: String): String = s"Example [$id] is not present in examples list"
def directoryExistsMsg(extractionTargetPath: String): String =
s"Can't download example, because extraction directory [$extractionTargetPath] already exist"

/**
* @return Seq of example names or Seq with path to parent dir where downloaded example was unpacked
*/
def init(
@mainargs.arg(positional = true, short = 'e') exampleId: Option[ExampleId],
@arg(name = "show-all") showAll: Flag = Flag()
): Command[Seq[String]] =
T.command {
usingExamples { examples =>
val result: Try[(Seq[String], String)] = exampleId match {
case None =>
val exampleIds: Seq[ExampleId] = examples.map { case (exampleId, _) => exampleId }
def fullMessage(exampleIds: Seq[ExampleId]) =
msg + "\n\n" + exampleIds.mkString("\n") + "\n\n" + msg
if (showAll.value)
Success((exampleIds, fullMessage(exampleIds)))
else {
val toShow = List("basic", "builds", "web")
val filteredIds =
exampleIds.filter(_.split('/').lift.apply(1).exists(toShow.contains))
Success((filteredIds, fullMessage(filteredIds)))
}
case Some(value) =>
val result: Try[(Seq[String], String)] = for {
url <- examples.toMap.get(value).toRight(new Exception(
moduleNotExistMsg(value)
)).toTry
extractedDirName = {
val zipName = url.split('/').last
if (zipName.toLowerCase.endsWith(".zip")) zipName.dropRight(4) else zipName
}
downloadDir = T.workspace
downloadPath = downloadDir / extractedDirName
_ <- if (os.exists(downloadPath)) Failure(new IOException(
directoryExistsMsg(downloadPath.toString)
))
else Success(())
path <-
Try({
val tmpName = UUID.randomUUID().toString + ".zip"
val downloaded = download(url, os.rel / tmpName)(downloadDir)
val unpacked = IO.unpackZip(downloaded.path, os.rel)(downloadDir)
Try(os.remove(downloaded.path))
unpacked
}).recoverWith(ex =>
Failure(
new IOException(s"Couldn't download example: [$value];\n ${ex.getMessage}")
)
)
} yield (Seq(path.path.toString()), s"Example downloaded to [$downloadPath]")

result
}
result
}.flatten match {
case Success((ret, msg)) =>
T.log.outputStream.println(msg)
ret
case Failure(exception) =>
T.log.error(exception.getMessage)
throw exception
}
}
private def usingExamples[T](fun: Seq[(ExampleId, ExampleUrl)] => T): Try[T] =
Using(getClass.getClassLoader.getResourceAsStream("exampleList.txt")) { exampleList =>
val reader = upickle.default.reader[Seq[(ExampleId, ExampleUrl)]]
val exampleNames: Seq[(ExampleId, ExampleUrl)] = upickle.default.read(exampleList)(reader)
fun(exampleNames)
}
}
Loading

0 comments on commit 5e51a3b

Please sign in to comment.