diff --git a/example/extending/evaluator/1-depmapper/build.mill b/example/extending/evaluator/1-depmapper/build.mill new file mode 100644 index 000000000000..91622564ea62 --- /dev/null +++ b/example/extending/evaluator/1-depmapper/build.mill @@ -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 --- + +*/ diff --git a/example/extending/evaluator/1-depmapper/foo/src/Foo.scala b/example/extending/evaluator/1-depmapper/foo/src/Foo.scala new file mode 100644 index 000000000000..2de577a0280f --- /dev/null +++ b/example/extending/evaluator/1-depmapper/foo/src/Foo.scala @@ -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) +} diff --git a/example/extending/evaluator/1-depmapper/foo/test/src/FooTests.scala b/example/extending/evaluator/1-depmapper/foo/test/src/FooTests.scala new file mode 100644 index 000000000000..9dcd8bf4e040 --- /dev/null +++ b/example/extending/evaluator/1-depmapper/foo/test/src/FooTests.scala @@ -0,0 +1,18 @@ +package foo + +import utest._ + +object FooTests extends TestSuite { + def tests = Tests { + test("simple") { + val result = Foo.generateHtml("hello") + assert(result == "

hello

") + result + } + test("escaping") { + val result = Foo.generateHtml("") + assert(result == "

<hello>

") + result + } + } +} diff --git a/example/extending/evaluator/2-unreferencedfiles/bar/src/Foo.scala b/example/extending/evaluator/2-unreferencedfiles/bar/src/Foo.scala new file mode 100644 index 000000000000..2de577a0280f --- /dev/null +++ b/example/extending/evaluator/2-unreferencedfiles/bar/src/Foo.scala @@ -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) +} diff --git a/example/extending/evaluator/2-unreferencedfiles/bar/test/src/FooTests.scala b/example/extending/evaluator/2-unreferencedfiles/bar/test/src/FooTests.scala new file mode 100644 index 000000000000..9dcd8bf4e040 --- /dev/null +++ b/example/extending/evaluator/2-unreferencedfiles/bar/test/src/FooTests.scala @@ -0,0 +1,18 @@ +package foo + +import utest._ + +object FooTests extends TestSuite { + def tests = Tests { + test("simple") { + val result = Foo.generateHtml("hello") + assert(result == "

hello

") + result + } + test("escaping") { + val result = Foo.generateHtml("") + assert(result == "

<hello>

") + result + } + } +} diff --git a/example/extending/evaluator/2-unreferencedfiles/build.mill b/example/extending/evaluator/2-unreferencedfiles/build.mill new file mode 100644 index 000000000000..5f0699579414 --- /dev/null +++ b/example/extending/evaluator/2-unreferencedfiles/build.mill @@ -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 + +*/ diff --git a/example/package.mill b/example/package.mill index 34caed3efcbe..07cd9c31823c 100644 --- a/example/package.mill +++ b/example/package.mill @@ -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 { diff --git a/website/docs/modules/ROOT/nav.adoc b/website/docs/modules/ROOT/nav.adoc index 60c77f748603..860a0fa93f31 100644 --- a/website/docs/modules/ROOT/nav.adoc +++ b/website/docs/modules/ROOT/nav.adoc @@ -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[] diff --git a/website/docs/modules/ROOT/pages/extending/evaluator.adoc b/website/docs/modules/ROOT/pages/extending/evaluator.adoc new file mode 100644 index 000000000000..0cb3bea64942 --- /dev/null +++ b/website/docs/modules/ROOT/pages/extending/evaluator.adoc @@ -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[] +