Skip to content

Evaluator API Commands Example doc #5204

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 79 additions & 0 deletions example/extending/evaluator/1-depmapper/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package build
import mill.*, scalalib.*

import mill.api._
import mill.api.daemon.SelectMode

object `package` extends ScalaModule {
def scalaVersion = "2.13.11"
def mvnDeps = Seq(
mvn"com.lihaoyi::scalatags:0.13.1",
mvn"com.lihaoyi::mainargs:0.6.2"
)

object test extends ScalaTests {
def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.8.5")
def testFramework = "utest.runner.Framework"
}

def depMapper(evaluator: Evaluator) = Task.Command(exclusive = true) {
val tasks = Seq("mvnDeps", "test.mvnDeps", "allSourceFiles")
val resolvedTasks = evaluator.resolveTasks(tasks, SelectMode.Multi).get
val executeResult = evaluator.execute(resolvedTasks)

executeResult.values match {
case mill.api.Result.Success(values) =>
val mainDeps = values(0).asInstanceOf[Seq[mill.javalib.Dep]]
val testDeps = values(1).asInstanceOf[Seq[mill.javalib.Dep]]
val sourceFiles = values(2).asInstanceOf[Seq[mill.api.PathRef]].map(_.path)

println("--- Dependency Users Report ---")
(mainDeps ++ testDeps).foreach { dep =>
val depName = s"${dep.organization}:${dep.name}:${dep.version}"
val usageInfo = findDependencyUsage(dep, sourceFiles)
if (usageInfo.nonEmpty) {
println(s"Dependency: $depName\nUsed By Files:")
usageInfo.foreach { case (file, imports) =>
println(s" - ${file.relativeTo(os.pwd)} (via: ${imports.mkString(", ")})")
}
println()
}
}
println("--- End Report ---")
case failure =>
println(s"Task execution failed: $failure")
}
}

def findDependencyUsage(
dep: mill.javalib.Dep,
sourceFiles: Seq[os.Path]
): Seq[(os.Path, Seq[String])] = {
sourceFiles.collect {
case file =>
val imports = extractImportsForDep(os.read(file), dep)
if (imports.nonEmpty) Some((file, imports)) else None
}.flatten
}

def extractImportsForDep(content: String, dep: mill.javalib.Dep): Seq[String] = {
val importRegex = """import\s+([^\s\n;]+)""".r

importRegex.findAllMatchIn(content)
.map(_.group(1))
.filter(_.startsWith(dep.name))
.toSeq
.distinct
}

}

// This command generates a report of dependency usage in source files.
// It uses resolveTasks and execute to gather information about dependencies and their usage in source files

/** Usage

> ./mill depMapper
--- Dependency Users Report ---

*/
16 changes: 16 additions & 0 deletions example/extending/evaluator/1-depmapper/foo/src/Foo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package foo
import scalatags.Text.all._
import mainargs.{main, ParserForMethods}

object Foo {
def generateHtml(text: String) = {
h1(text).toString
}

@main
def main(text: String) = {
println(generateHtml(text))
}

def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package foo

import utest._

object FooTests extends TestSuite {
def tests = Tests {
test("simple") {
val result = Foo.generateHtml("hello")
assert(result == "<h1>hello</h1>")
result
}
test("escaping") {
val result = Foo.generateHtml("<hello>")
assert(result == "<h1>&lt;hello&gt;</h1>")
result
}
}
}
16 changes: 16 additions & 0 deletions example/extending/evaluator/2-unreferencedfiles/bar/src/Foo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package foo
import scalatags.Text.all._
import mainargs.{main, ParserForMethods}

object Foo {
def generateHtml(text: String) = {
h1(text).toString
}

@main
def main(text: String) = {
println(generateHtml(text))
}

def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package foo

import utest._

object FooTests extends TestSuite {
def tests = Tests {
test("simple") {
val result = Foo.generateHtml("hello")
assert(result == "<h1>hello</h1>")
result
}
test("escaping") {
val result = Foo.generateHtml("<hello>")
assert(result == "<h1>&lt;hello&gt;</h1>")
result
}
}
}
90 changes: 90 additions & 0 deletions example/extending/evaluator/2-unreferencedfiles/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package build
import mill.*, scalalib.*

import mill.api._
import mill.api.daemon.SelectMode

object `package` extends ScalaModule {
def scalaVersion = "2.13.11"
def mvnDeps = Seq(
mvn"com.lihaoyi::scalatags:0.13.1",
mvn"com.lihaoyi::mainargs:0.6.2"
)

object test extends ScalaTests {
def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.8.5")
def testFramework = "utest.runner.Framework"
}

def unreferencedFiles(evaluator: Evaluator) = Task.Command(exclusive = true) {
val sourceFiles = Seq("allSourceFiles")
val segmentResult = evaluator.resolveSegments(sourceFiles, SelectMode.Multi).get
segmentResult.foreach { segment =>
println(s"Planning segment: ${segment.render}")
}

val resolveResult = evaluator.resolveTasks(sourceFiles, SelectMode.Multi).get
val plan = evaluator.plan(resolveResult)
.sortedGroups
.keys()
.map(_.toString)
.toIndexedSeq
plan.foreach(task => println(s"Planned task: $task"))

val executeResult = evaluator.evaluate(plan, SelectMode.Multi).get

val knownSources = executeResult.values match {
case mill.api.Result.Success(resultVector) =>
val allPaths = for {
resultList <- resultVector.asInstanceOf[Vector[List[Any]]]
item <- resultList
pathRef <- item match {
case p: mill.api.PathRef => Some(p.path)
case _ => None
}
} yield pathRef

println(s"Extracted ${allPaths.size} known source paths")
allPaths.toSet

case mill.api.Result.Failure(msg) =>
println(s"Task execution failed: $msg")
Set.empty[os.Path]
}

// Find all source files on disk
val projectRoot = os.pwd
val sourceExtensions = Set(".scala")
val diskSources = os.walk(projectRoot)
.filter(p => sourceExtensions.exists(p.toString.endsWith))
.filter(!_.segments.contains(".git"))
.filter(!_.segments.contains("out"))
.toSet

// Find unreferenced files
val unreferenced = diskSources -- knownSources
if (unreferenced.nonEmpty) {
println("--- Unreferenced Source Files ---")
unreferenced.toSeq.sorted.foreach { file =>
println(s" - ${file.relativeTo(projectRoot)}")
}
println(s"\nTotal: ${unreferenced.size} unreferenced files")
} else {
println("No unreferenced source files found!")
}
}

}

// This command finds source files that are not referenced by any module in the Mill build.
// It uses resolveSegments, resolveTasks, plan and evaluate to gather information about source files
// and their dependencies.
// It also excludes files in the .git directory and Mill's output directory.
// It prints a report of unreferenced files.

/** Usage

> ./mill unreferencedFiles
Extracted 2 known source paths

*/
1 change: 1 addition & 0 deletions example/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ object `package` extends Module {
object jvmcode extends Cross[ExampleCrossModule](build.listCross)
object python extends Cross[ExampleCrossModule](build.listCross)
object typescript extends Cross[ExampleCrossModule](build.listCross)
object evaluator extends Cross[ExampleCrossModule](build.listCross)
}

trait ExampleCrossModuleKotlin extends ExampleCrossModuleJava {
Expand Down
1 change: 1 addition & 0 deletions website/docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
** xref:extending/meta-build.adoc[]
** xref:extending/example-typescript-support.adoc[]
** xref:extending/example-python-support.adoc[]
** xref:extending/evaluator.adoc[]
* xref:large/large.adoc[]
** xref:large/selective-execution.adoc[]
** xref:large/multi-file-builds.adoc[]
Expand Down
32 changes: 32 additions & 0 deletions website/docs/modules/ROOT/pages/extending/evaluator.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
= Example: Evaluator API Commands

The Mill `Evaluator` API provides programmatic access to Mill's core functionalities, allowing you to resolve, plan, and execute build tasks directly.
This API is essential for extending Mill built-in features through interaction and control of the build process.

== Dependency Mapper

In this example, the `depMapper` task demonstrates how to use the `Evaluator` API to resolve and execute multiple build tasks,
then analyze and report on dependency usage within your source files using both `resolveTasks` and `execute`.

This task:

* Resolves the main and test dependencies, as well as all source files using `resolveTasks`.
* Executes these tasks to gather the actual dependencies values and source file paths with `execute`.
* Scans each source file for import statements that match the resolved dependencies with helper methods.
* Prints a report showing which dependencies are used by which source files, including the specific import statements.

include::partial$example/extending/evaluator/1-depMapper.adoc[]

== Orphaned Source Files
In this example, the `unreferencedFiles` task shows how to use the `Evaluator` API to find source files in your project that are not referenced by any module.

This task:

* Resolves all known source files using `resolveSegments` and `resolveTasks`.
* Plans and evaluates these tasks to collect the set of referenced source file paths using `plan` and `evaluate`.
* Walks the project directory to find all `.scala` source files, excluding `.git` and Mill's output directories.
* Compares the discovered files on disk with the referenced files to identify unreferenced (orphaned) source files.
* Prints a report listing all orphaned files, or a message if none are found.

include::partial$example/extending/evaluator/2-unreferencedfiles.adoc[]

Loading