Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use mill examples in init #3583

Merged
merged 47 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
29851e7
modifying init to fetch examples from releases page instead of using …
pawelsadlo Sep 20, 2024
68260e3
renaming init -> initmodule
pawelsadlo Sep 20, 2024
eaa5402
fix integration tests
pawelsadlo Sep 20, 2024
bf479c0
format
pawelsadlo Sep 20, 2024
838dab0
fixing tests
pawelsadlo Sep 21, 2024
2947bb9
fix
pawelsadlo Sep 21, 2024
40216f7
fix
pawelsadlo Sep 21, 2024
fdcef78
fix
pawelsadlo Sep 21, 2024
f72e879
fix
pawelsadlo Sep 21, 2024
605089a
Update build.mill
pawelsadlo Sep 21, 2024
185fdf9
fix
pawelsadlo Sep 21, 2024
e5b9756
Merge remote-tracking branch 'ps_mill/list-examples' into list-examples
pawelsadlo Sep 21, 2024
bd33abf
fix
pawelsadlo Sep 21, 2024
039afcc
fix
pawelsadlo Sep 21, 2024
c7623ac
fix
pawelsadlo Sep 21, 2024
17c1b81
fix
pawelsadlo Sep 21, 2024
4b0bcbb
Update build.mill
pawelsadlo Sep 21, 2024
2ea02db
fix
pawelsadlo Sep 23, 2024
d4997a7
Merge remote-tracking branch 'ps_mill/list-examples' into list-examples
pawelsadlo Sep 23, 2024
a746cda
fix
pawelsadlo Sep 23, 2024
ecf5a65
format
pawelsadlo Sep 23, 2024
83244ce
sorting exampleList
pawelsadlo Sep 26, 2024
86b30ea
reverting forVersion param from examplePathsWithArtifactName
pawelsadlo Sep 26, 2024
52fb129
add show-all param handling
pawelsadlo Sep 26, 2024
c1d3bf8
format
pawelsadlo Sep 26, 2024
0eb5c0d
Merge branch 'main' into list-examples
pawelsadlo Sep 26, 2024
f5a52ed
Update MainModule.scala
pawelsadlo Sep 26, 2024
fc84b10
fix sorting
pawelsadlo Sep 26, 2024
45b45ef
Merge remote-tracking branch 'ps_mill/list-examples' into list-examples
pawelsadlo Sep 26, 2024
5badd4c
adding test for overriding directory while downloading example,
pawelsadlo Sep 27, 2024
505c588
Merge remote-tracking branch 'refs/remotes/origin/main' into list-exa…
pawelsadlo Sep 27, 2024
7081e4e
fixing download url
pawelsadlo Sep 27, 2024
38da55f
Revert "fixing download url"
pawelsadlo Sep 27, 2024
4731e07
adjusting for CI
pawelsadlo Sep 27, 2024
eadbab0
Revert "adjusting for CI"
pawelsadlo Sep 27, 2024
6b87184
parsing millLastTag from version
pawelsadlo Sep 28, 2024
5bc0e1b
Merge branch 'main' into list-examples
pawelsadlo Sep 28, 2024
738493d
Merge branch 'main' into list-examples
lihaoyi Sep 30, 2024
b067dbe
Merge branch 'main' into 3583
lihaoyi Sep 30, 2024
bb1d96c
merge
lihaoyi Sep 30, 2024
fd9a276
package.mill
lihaoyi Sep 30, 2024
542f339
format
lihaoyi Sep 30, 2024
ba9d622
reorg
lihaoyi Sep 30, 2024
ef8c3a0
repro line prefix double newline issue
lihaoyi Sep 30, 2024
28d0611
fix for LinePrefixOutputStream double newline
lihaoyi Sep 30, 2024
e3a0b69
fmt
lihaoyi Sep 30, 2024
7849933
.
lihaoyi Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading