From ae9d7ae1d35c5106da05bae2fd7be84c4cde860c Mon Sep 17 00:00:00 2001 From: c0d33ngr Date: Tue, 27 May 2025 06:23:45 +0100 Subject: [PATCH 1/8] initial commit --- .../evaluator/1-todo-webapp-report/build.mill | 115 ++++++ .../resources/webapp/index.css | 378 ++++++++++++++++++ .../resources/webapp/main.js | 70 ++++ .../1-todo-webapp-report/src/WebApp.scala | 137 +++++++ .../test/src/WebAppTests.scala | 24 ++ website/docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/extending/evaluator.adoc | 5 + 7 files changed, 730 insertions(+) create mode 100644 example/extending/evaluator/1-todo-webapp-report/build.mill create mode 100644 example/extending/evaluator/1-todo-webapp-report/resources/webapp/index.css create mode 100644 example/extending/evaluator/1-todo-webapp-report/resources/webapp/main.js create mode 100644 example/extending/evaluator/1-todo-webapp-report/src/WebApp.scala create mode 100644 example/extending/evaluator/1-todo-webapp-report/test/src/WebAppTests.scala create mode 100644 website/docs/modules/ROOT/pages/extending/evaluator.adoc diff --git a/example/extending/evaluator/1-todo-webapp-report/build.mill b/example/extending/evaluator/1-todo-webapp-report/build.mill new file mode 100644 index 000000000000..143152f5e2ce --- /dev/null +++ b/example/extending/evaluator/1-todo-webapp-report/build.mill @@ -0,0 +1,115 @@ +package build +import mill.*, scalalib.* + +import mill.define._ +import mill.define.SelectMode +import mill.api.Result.create +import mill.api.Result + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +object `package` extends ScalaModule { + def scalaVersion = "2.13.8" + def mvnDeps = Seq( + mvn"com.lihaoyi::cask:0.9.1", + mvn"com.lihaoyi::scalatags:0.13.1" + ) + + object test extends ScalaTests with TestModule.Utest { + def mvnDeps = Seq( + mvn"com.lihaoyi::utest::0.8.5", + mvn"com.lihaoyi::requests::0.6.9" + ) + } + + // Helper function to format bytes into a human-readable string (KB, MB, GB) + def formatBytes(bytes: Long): String = { + if (bytes < 1024) s"$bytes B" + else { + val kilobyte = 1024.0 + val megabyte = kilobyte * 1024 + val gigabyte = megabyte * 1024 + + if (bytes < megabyte) f"${bytes / kilobyte}%.1f KB" + else if (bytes < gigabyte) f"${bytes / megabyte}%.1f MB" + else f"${bytes / gigabyte}%.1f GB" + } + } + + val bundleArgs: Seq[String] = Seq("assembly", "test.assembly") + val buildArgs: Seq[String] = Seq("compile", "test.compile") + + def buildReport(evaluator: Evaluator) = Task.Command(exclusive = true) { + + println("=== Build Report ===") + val now = LocalDateTime.now() + val formatted = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + println(s"Build Report started at: $formatted") + println("=================") + + println(s"\nBundle Size Analysis:") + println(s"---------------------") + + val resolvedBundleTasks = evaluator.resolveTasks(bundleArgs, SelectMode.Multi).get + + val execBundleResults = evaluator.execute(resolvedBundleTasks).values.get + + execBundleResults.foreach { bundleResult => + val jarPathStr = bundleResult.toString.split(":", 4).last + val jarPath = os.Path(jarPathStr) + + println(s"Checking JAR file: ${jarPath}") + println(s"JAR size is ${formatBytes(os.size(jarPath))}") + } + + println(s"\nBuild Performance Profile:") + println(s"---------------------") + + val planStartTime = System.currentTimeMillis() + + println(s"\n--- Resolution and Planning Phase ---") + println(s"Resolved tasks (for planning): ${resolvedBundleTasks}") + + val plan = evaluator.plan(resolvedBundleTasks) + .sortedGroups + .keys() + .map(_.toString) + .toArray + + val planEndTime = System.currentTimeMillis() + + println(s"Planning time: ${planEndTime - planStartTime} ms") + println(s"Total planned tasks: ${plan.length}") + + val execStartTime = System.currentTimeMillis() + val resolvedSegmentResults = evaluator.resolveSegments(buildArgs, SelectMode.Multi).get + + println(s"\n--- Execution Phase ---") + + val labelNames = resolvedSegmentResults.flatMap(_.parts) // Extract names for evaluate + println(s"Resolved segments label names (for execution): ${labelNames}") + + val evalResults = evaluator.evaluate(labelNames, SelectMode.Multi) + val execEndTime = System.currentTimeMillis() + + println(s"Execution time: ${execEndTime - execStartTime} ms") + println(s"Total command time: ${execEndTime - planStartTime} ms") // Overall time + + () + } + +} + +// This build file defines a simple web application using Cask and Scalatags. +// It includes a build report that analyzes the bundle size and build performance. +// The report includes the size of the generated JAR files and the time taken for resolution, planning, and execution phases. +// The build report can be executed with the command `mill buildReport`. + +/** Usage + +> ./mill buildReport + +=== Build Report === + +*/ diff --git a/example/extending/evaluator/1-todo-webapp-report/resources/webapp/index.css b/example/extending/evaluator/1-todo-webapp-report/resources/webapp/index.css new file mode 100644 index 000000000000..07ef4a160e3b --- /dev/null +++ b/example/extending/evaluator/1-todo-webapp-report/resources/webapp/index.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +.toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +.toggle-all:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todo-list li label { + white-space: pre-line; + word-break: break-all; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a.selected, +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } + + .toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/example/extending/evaluator/1-todo-webapp-report/resources/webapp/main.js b/example/extending/evaluator/1-todo-webapp-report/resources/webapp/main.js new file mode 100644 index 000000000000..4a7617f50c2b --- /dev/null +++ b/example/extending/evaluator/1-todo-webapp-report/resources/webapp/main.js @@ -0,0 +1,70 @@ +var state = "all"; + +var todoApp = document.getElementsByClassName("todoapp")[0]; +function postFetchUpdate(url){ + fetch(url, { + method: "POST", + }) + .then(function(response){ return response.text()}) + .then(function (text) { + todoApp.innerHTML = text; + initListeners() + }) +} + +function bindEvent(cls, url, endState){ + + document.getElementsByClassName(cls)[0].addEventListener( + "mousedown", + function(evt){ + postFetchUpdate(url) + if (endState) state = endState + } + ); +} + +function bindIndexedEvent(cls, func){ + Array.from(document.getElementsByClassName(cls)).forEach( function(elem) { + elem.addEventListener( + "mousedown", + function(evt){ + postFetchUpdate(func(elem.getAttribute("data-todo-index"))) + } + ) + }); +} + +function initListeners(){ + bindIndexedEvent( + "destroy", + function(index){return "/delete/" + state + "/" + index} + ); + bindIndexedEvent( + "toggle", + function(index){return "/toggle/" + state + "/" + index} + ); + bindEvent("toggle-all", "/toggle-all/" + state); + bindEvent("todo-all", "/list/all", "all"); + bindEvent("todo-active", "/list/active", "active"); + bindEvent("todo-completed", "/list/completed", "completed"); + bindEvent("clear-completed", "/clear-completed/" + state); + var newTodoInput = document.getElementsByClassName("new-todo")[0]; + newTodoInput.addEventListener( + "keydown", + function(evt){ + if (evt.keyCode === 13) { + fetch("/add/" + state, { + method: "POST", + body: newTodoInput.value + }) + .then(function(response){ return response.text()}) + .then(function (text) { + newTodoInput.value = ""; + todoApp.innerHTML = text; + initListeners() + }) + } + } + ); +} +initListeners() \ No newline at end of file diff --git a/example/extending/evaluator/1-todo-webapp-report/src/WebApp.scala b/example/extending/evaluator/1-todo-webapp-report/src/WebApp.scala new file mode 100644 index 000000000000..30787dcd037f --- /dev/null +++ b/example/extending/evaluator/1-todo-webapp-report/src/WebApp.scala @@ -0,0 +1,137 @@ +package webapp +import scalatags.Text.all._ +import scalatags.Text.tags2 + +object WebApp extends cask.MainRoutes { + case class Todo(checked: Boolean, text: String) + + object Todo { + implicit def todoRW: upickle.default.ReadWriter[Todo] = upickle.default.macroRW[Todo] + } + + var todos = Seq( + Todo(true, "Get started with Cask"), + Todo(false, "Profit!") + ) + + @cask.post("/list/:state") + def list(state: String) = renderBody(state) + + @cask.post("/add/:state") + def add(state: String, request: cask.Request) = { + todos = Seq(Todo(false, request.text())) ++ todos + renderBody(state) + } + + @cask.post("/delete/:state/:index") + def delete(state: String, index: Int) = { + todos = todos.patch(index, Nil, 1) + renderBody(state) + } + + @cask.post("/toggle/:state/:index") + def toggle(state: String, index: Int) = { + todos = todos.updated(index, todos(index).copy(checked = !todos(index).checked)) + renderBody(state) + } + + @cask.post("/clear-completed/:state") + def clearCompleted(state: String) = { + todos = todos.filter(!_.checked) + renderBody(state) + } + + @cask.post("/toggle-all/:state") + def toggleAll(state: String) = { + val next = todos.filter(_.checked).size != 0 + todos = todos.map(_.copy(checked = next)) + renderBody(state) + } + + def renderBody(state: String) /*: scalatags.Text.TypedTag[String] */ = { + val filteredTodos = state match { + case "all" => todos.zipWithIndex + case "active" => todos.zipWithIndex.filter(!_._1.checked) + case "completed" => todos.zipWithIndex.filter(_._1.checked) + } + div( + header( + cls := "header", + h1("todos"), + input(cls := "new-todo", placeholder := "What needs to be done?", autofocus := "") + ), + tags2.section( + cls := "main", + input( + id := "toggle-all", + cls := "toggle-all", + `type` := "checkbox", + if (todos.filter(_.checked).size != 0) checked else () + ), + label(`for` := "toggle-all", "Mark all as complete"), + ul( + cls := "todo-list", + for ((todo, index) <- filteredTodos) yield li( + if (todo.checked) cls := "completed" else (), + div( + cls := "view", + input( + cls := "toggle", + `type` := "checkbox", + if (todo.checked) checked else (), + data("todo-index") := index + ), + label(todo.text), + button(cls := "destroy", data("todo-index") := index) + ), + input(cls := "edit", value := todo.text) + ) + ) + ), + footer( + cls := "footer", + span(cls := "todo-count", strong(todos.filter(!_.checked).size), " items left"), + ul( + cls := "filters", + li(cls := "todo-all", a(if (state == "all") cls := "selected" else (), "All")), + li(cls := "todo-active", a(if (state == "active") cls := "selected" else (), "Active")), + li( + cls := "todo-completed", + a(if (state == "completed") cls := "selected" else (), "Completed") + ) + ), + button(cls := "clear-completed", "Clear completed") + ) + ) + } + + @cask.get("/") + def index() = { + doctype("html")( + html( + lang := "en", + head( + meta(charset := "utf-8"), + meta(name := "viewport", content := "width=device-width, initial-scale=1"), + tags2.title("Template • TodoMVC"), + link(rel := "stylesheet", href := "/static/index.css") + ), + body( + tags2.section(cls := "todoapp", renderBody("all")), + footer( + cls := "info", + p("Double-click to edit a todo"), + p("Created by ", a(href := "http://todomvc.com", "Li Haoyi")), + p("Part of ", a(href := "http://todomvc.com", "TodoMVC")) + ), + script(src := "/static/main.js") + ) + ) + ) + } + + @cask.staticResources("/static") + def static() = "webapp" + + initialize() +} diff --git a/example/extending/evaluator/1-todo-webapp-report/test/src/WebAppTests.scala b/example/extending/evaluator/1-todo-webapp-report/test/src/WebAppTests.scala new file mode 100644 index 000000000000..c17e542f7812 --- /dev/null +++ b/example/extending/evaluator/1-todo-webapp-report/test/src/WebAppTests.scala @@ -0,0 +1,24 @@ +package webapp + +import utest._ + +object WebAppTests extends TestSuite { + def withServer[T](example: cask.main.Main)(f: String => T): T = { + val server = io.undertow.Undertow.builder + .addHttpListener(8181, "localhost") + .setHandler(example.defaultHandler) + .build + server.start() + val res = + try f("http://localhost:8181") + finally server.stop() + res + } + + val tests = Tests { + test("simpleRequest") - withServer(WebApp) { host => + val page = requests.get(host).text() + assert(page.contains("What needs to be done?")) + } + } +} 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..ae924fd9471e --- /dev/null +++ b/website/docs/modules/ROOT/pages/extending/evaluator.adoc @@ -0,0 +1,5 @@ += Examples Documentation of usong Evaluator API Commands + +== TodoMVC WebApp Build Report + +include::partial$example/extending/evaluator/1-todo-webapp-report.adoc[] \ No newline at end of file From 7466af86d2ba3ca02477e36820a6cfa4d437744f Mon Sep 17 00:00:00 2001 From: c0d33ngr Date: Thu, 29 May 2025 11:10:35 +0100 Subject: [PATCH 2/8] fix ci error --- example/extending/evaluator/1-todo-webapp-report/build.mill | 1 - 1 file changed, 1 deletion(-) diff --git a/example/extending/evaluator/1-todo-webapp-report/build.mill b/example/extending/evaluator/1-todo-webapp-report/build.mill index 143152f5e2ce..42514d9721b8 100644 --- a/example/extending/evaluator/1-todo-webapp-report/build.mill +++ b/example/extending/evaluator/1-todo-webapp-report/build.mill @@ -109,7 +109,6 @@ object `package` extends ScalaModule { /** Usage > ./mill buildReport - === Build Report === */ From e80028987156fc410be690622710e30b5bbcaefa Mon Sep 17 00:00:00 2001 From: c0d33ngr Date: Mon, 2 Jun 2025 12:10:19 +0100 Subject: [PATCH 3/8] doc draft --- .../evaluator/1-resolve-segments/build.mill | 43 ++ .../1-resolve-segments/foo/src/Foo.scala | 16 + .../foo/test/src/FooTests.scala | 18 + .../evaluator/1-todo-webapp-report/build.mill | 114 ------ .../resources/webapp/index.css | 378 ------------------ .../resources/webapp/main.js | 70 ---- .../1-todo-webapp-report/src/WebApp.scala | 137 ------- .../test/src/WebAppTests.scala | 24 -- .../evaluator/2-resolve-tasks/build.mill | 44 ++ .../2-resolve-tasks/foo/src/Foo.scala | 16 + .../foo/test/src/FooTests.scala | 18 + example/extending/evaluator/3-plan/build.mill | 48 +++ .../evaluator/3-plan/foo/src/Foo.scala | 16 + .../3-plan/foo/test/src/FooTests.scala | 18 + .../extending/evaluator/4-execute/build.mill | 44 ++ .../evaluator/4-execute/foo/src/Foo.scala | 16 + .../4-execute/foo/test/src/FooTests.scala | 18 + .../extending/evaluator/5-evaluate/build.mill | 41 ++ .../evaluator/5-evaluate/foo/src/Foo.scala | 16 + .../5-evaluate/foo/test/src/FooTests.scala | 18 + .../ROOT/pages/extending/evaluator.adoc | 89 ++++- 21 files changed, 476 insertions(+), 726 deletions(-) create mode 100644 example/extending/evaluator/1-resolve-segments/build.mill create mode 100644 example/extending/evaluator/1-resolve-segments/foo/src/Foo.scala create mode 100644 example/extending/evaluator/1-resolve-segments/foo/test/src/FooTests.scala delete mode 100644 example/extending/evaluator/1-todo-webapp-report/build.mill delete mode 100644 example/extending/evaluator/1-todo-webapp-report/resources/webapp/index.css delete mode 100644 example/extending/evaluator/1-todo-webapp-report/resources/webapp/main.js delete mode 100644 example/extending/evaluator/1-todo-webapp-report/src/WebApp.scala delete mode 100644 example/extending/evaluator/1-todo-webapp-report/test/src/WebAppTests.scala create mode 100644 example/extending/evaluator/2-resolve-tasks/build.mill create mode 100644 example/extending/evaluator/2-resolve-tasks/foo/src/Foo.scala create mode 100644 example/extending/evaluator/2-resolve-tasks/foo/test/src/FooTests.scala create mode 100644 example/extending/evaluator/3-plan/build.mill create mode 100644 example/extending/evaluator/3-plan/foo/src/Foo.scala create mode 100644 example/extending/evaluator/3-plan/foo/test/src/FooTests.scala create mode 100644 example/extending/evaluator/4-execute/build.mill create mode 100644 example/extending/evaluator/4-execute/foo/src/Foo.scala create mode 100644 example/extending/evaluator/4-execute/foo/test/src/FooTests.scala create mode 100644 example/extending/evaluator/5-evaluate/build.mill create mode 100644 example/extending/evaluator/5-evaluate/foo/src/Foo.scala create mode 100644 example/extending/evaluator/5-evaluate/foo/test/src/FooTests.scala diff --git a/example/extending/evaluator/1-resolve-segments/build.mill b/example/extending/evaluator/1-resolve-segments/build.mill new file mode 100644 index 000000000000..ea381d474016 --- /dev/null +++ b/example/extending/evaluator/1-resolve-segments/build.mill @@ -0,0 +1,43 @@ +package build +import mill.*, scalalib.* + +import mill.define._ +import mill.define.SelectMode +import mill.api.Result.create +import mill.api.Result + +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 customSegmentCommand(evaluator: Evaluator, tasks: String*) = Task.Command(exclusive = true) { + val segmentsResult = evaluator + .resolveSegments(tasks, SelectMode.Multi) + .get + + segmentsResult.foreach { segment => + println(s"Segment: ${segment.parts.mkString(", ")}") + } + () + } + +} + +// This command resolves the segments of the given tasks and prints them. +// The segments are the parts of the task names that are used to group tasks. + +/** Usage + +> ./mill customSegmentCommand compile test +Segment: compile +Segment: test + +*/ diff --git a/example/extending/evaluator/1-resolve-segments/foo/src/Foo.scala b/example/extending/evaluator/1-resolve-segments/foo/src/Foo.scala new file mode 100644 index 000000000000..2de577a0280f --- /dev/null +++ b/example/extending/evaluator/1-resolve-segments/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-resolve-segments/foo/test/src/FooTests.scala b/example/extending/evaluator/1-resolve-segments/foo/test/src/FooTests.scala new file mode 100644 index 000000000000..9dcd8bf4e040 --- /dev/null +++ b/example/extending/evaluator/1-resolve-segments/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/1-todo-webapp-report/build.mill b/example/extending/evaluator/1-todo-webapp-report/build.mill deleted file mode 100644 index 42514d9721b8..000000000000 --- a/example/extending/evaluator/1-todo-webapp-report/build.mill +++ /dev/null @@ -1,114 +0,0 @@ -package build -import mill.*, scalalib.* - -import mill.define._ -import mill.define.SelectMode -import mill.api.Result.create -import mill.api.Result - -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -object `package` extends ScalaModule { - def scalaVersion = "2.13.8" - def mvnDeps = Seq( - mvn"com.lihaoyi::cask:0.9.1", - mvn"com.lihaoyi::scalatags:0.13.1" - ) - - object test extends ScalaTests with TestModule.Utest { - def mvnDeps = Seq( - mvn"com.lihaoyi::utest::0.8.5", - mvn"com.lihaoyi::requests::0.6.9" - ) - } - - // Helper function to format bytes into a human-readable string (KB, MB, GB) - def formatBytes(bytes: Long): String = { - if (bytes < 1024) s"$bytes B" - else { - val kilobyte = 1024.0 - val megabyte = kilobyte * 1024 - val gigabyte = megabyte * 1024 - - if (bytes < megabyte) f"${bytes / kilobyte}%.1f KB" - else if (bytes < gigabyte) f"${bytes / megabyte}%.1f MB" - else f"${bytes / gigabyte}%.1f GB" - } - } - - val bundleArgs: Seq[String] = Seq("assembly", "test.assembly") - val buildArgs: Seq[String] = Seq("compile", "test.compile") - - def buildReport(evaluator: Evaluator) = Task.Command(exclusive = true) { - - println("=== Build Report ===") - val now = LocalDateTime.now() - val formatted = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - println(s"Build Report started at: $formatted") - println("=================") - - println(s"\nBundle Size Analysis:") - println(s"---------------------") - - val resolvedBundleTasks = evaluator.resolveTasks(bundleArgs, SelectMode.Multi).get - - val execBundleResults = evaluator.execute(resolvedBundleTasks).values.get - - execBundleResults.foreach { bundleResult => - val jarPathStr = bundleResult.toString.split(":", 4).last - val jarPath = os.Path(jarPathStr) - - println(s"Checking JAR file: ${jarPath}") - println(s"JAR size is ${formatBytes(os.size(jarPath))}") - } - - println(s"\nBuild Performance Profile:") - println(s"---------------------") - - val planStartTime = System.currentTimeMillis() - - println(s"\n--- Resolution and Planning Phase ---") - println(s"Resolved tasks (for planning): ${resolvedBundleTasks}") - - val plan = evaluator.plan(resolvedBundleTasks) - .sortedGroups - .keys() - .map(_.toString) - .toArray - - val planEndTime = System.currentTimeMillis() - - println(s"Planning time: ${planEndTime - planStartTime} ms") - println(s"Total planned tasks: ${plan.length}") - - val execStartTime = System.currentTimeMillis() - val resolvedSegmentResults = evaluator.resolveSegments(buildArgs, SelectMode.Multi).get - - println(s"\n--- Execution Phase ---") - - val labelNames = resolvedSegmentResults.flatMap(_.parts) // Extract names for evaluate - println(s"Resolved segments label names (for execution): ${labelNames}") - - val evalResults = evaluator.evaluate(labelNames, SelectMode.Multi) - val execEndTime = System.currentTimeMillis() - - println(s"Execution time: ${execEndTime - execStartTime} ms") - println(s"Total command time: ${execEndTime - planStartTime} ms") // Overall time - - () - } - -} - -// This build file defines a simple web application using Cask and Scalatags. -// It includes a build report that analyzes the bundle size and build performance. -// The report includes the size of the generated JAR files and the time taken for resolution, planning, and execution phases. -// The build report can be executed with the command `mill buildReport`. - -/** Usage - -> ./mill buildReport -=== Build Report === - -*/ diff --git a/example/extending/evaluator/1-todo-webapp-report/resources/webapp/index.css b/example/extending/evaluator/1-todo-webapp-report/resources/webapp/index.css deleted file mode 100644 index 07ef4a160e3b..000000000000 --- a/example/extending/evaluator/1-todo-webapp-report/resources/webapp/index.css +++ /dev/null @@ -1,378 +0,0 @@ -html, -body { - margin: 0; - padding: 0; -} - -button { - margin: 0; - padding: 0; - border: 0; - background: none; - font-size: 100%; - vertical-align: baseline; - font-family: inherit; - font-weight: inherit; - color: inherit; - -webkit-appearance: none; - appearance: none; - -webkit-font-smoothing: antialiased; - -moz-font-smoothing: antialiased; - font-smoothing: antialiased; -} - -body { - font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; - line-height: 1.4em; - background: #f5f5f5; - color: #4d4d4d; - min-width: 230px; - max-width: 550px; - margin: 0 auto; - -webkit-font-smoothing: antialiased; - -moz-font-smoothing: antialiased; - font-smoothing: antialiased; - font-weight: 300; -} - -button, -input[type="checkbox"] { - outline: none; -} - -.hidden { - display: none; -} - -.todoapp { - background: #fff; - margin: 130px 0 40px 0; - position: relative; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), - 0 25px 50px 0 rgba(0, 0, 0, 0.1); -} - -.todoapp input::-webkit-input-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::-moz-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::input-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp h1 { - position: absolute; - top: -155px; - width: 100%; - font-size: 100px; - font-weight: 100; - text-align: center; - color: rgba(175, 47, 47, 0.15); - -webkit-text-rendering: optimizeLegibility; - -moz-text-rendering: optimizeLegibility; - text-rendering: optimizeLegibility; -} - -.new-todo, -.edit { - position: relative; - margin: 0; - width: 100%; - font-size: 24px; - font-family: inherit; - font-weight: inherit; - line-height: 1.4em; - border: 0; - outline: none; - color: inherit; - padding: 6px; - border: 1px solid #999; - box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); - box-sizing: border-box; - -webkit-font-smoothing: antialiased; - -moz-font-smoothing: antialiased; - font-smoothing: antialiased; -} - -.new-todo { - padding: 16px 16px 16px 60px; - border: none; - background: rgba(0, 0, 0, 0.003); - box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); -} - -.main { - position: relative; - z-index: 2; - border-top: 1px solid #e6e6e6; -} - -label[for='toggle-all'] { - display: none; -} - -.toggle-all { - position: absolute; - top: -55px; - left: -12px; - width: 60px; - height: 34px; - text-align: center; - border: none; /* Mobile Safari */ -} - -.toggle-all:before { - content: '❯'; - font-size: 22px; - color: #e6e6e6; - padding: 10px 27px 10px 27px; -} - -.toggle-all:checked:before { - color: #737373; -} - -.todo-list { - margin: 0; - padding: 0; - list-style: none; -} - -.todo-list li { - position: relative; - font-size: 24px; - border-bottom: 1px solid #ededed; -} - -.todo-list li:last-child { - border-bottom: none; -} - -.todo-list li.editing { - border-bottom: none; - padding: 0; -} - -.todo-list li.editing .edit { - display: block; - width: 506px; - padding: 13px 17px 12px 17px; - margin: 0 0 0 43px; -} - -.todo-list li.editing .view { - display: none; -} - -.todo-list li .toggle { - text-align: center; - width: 40px; - /* auto, since non-WebKit browsers doesn't support input styling */ - height: auto; - position: absolute; - top: 0; - bottom: 0; - margin: auto 0; - border: none; /* Mobile Safari */ - -webkit-appearance: none; - appearance: none; -} - -.todo-list li .toggle:after { - content: url('data:image/svg+xml;utf8,'); -} - -.todo-list li .toggle:checked:after { - content: url('data:image/svg+xml;utf8,'); -} - -.todo-list li label { - white-space: pre-line; - word-break: break-all; - padding: 15px 60px 15px 15px; - margin-left: 45px; - display: block; - line-height: 1.2; - transition: color 0.4s; -} - -.todo-list li.completed label { - color: #d9d9d9; - text-decoration: line-through; -} - -.todo-list li .destroy { - display: none; - position: absolute; - top: 0; - right: 10px; - bottom: 0; - width: 40px; - height: 40px; - margin: auto 0; - font-size: 30px; - color: #cc9a9a; - margin-bottom: 11px; - transition: color 0.2s ease-out; -} - -.todo-list li .destroy:hover { - color: #af5b5e; -} - -.todo-list li .destroy:after { - content: '×'; -} - -.todo-list li:hover .destroy { - display: block; -} - -.todo-list li .edit { - display: none; -} - -.todo-list li.editing:last-child { - margin-bottom: -1px; -} - -.footer { - color: #777; - padding: 10px 15px; - height: 20px; - text-align: center; - border-top: 1px solid #e6e6e6; -} - -.footer:before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - left: 0; - height: 50px; - overflow: hidden; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), - 0 8px 0 -3px #f6f6f6, - 0 9px 1px -3px rgba(0, 0, 0, 0.2), - 0 16px 0 -6px #f6f6f6, - 0 17px 2px -6px rgba(0, 0, 0, 0.2); -} - -.todo-count { - float: left; - text-align: left; -} - -.todo-count strong { - font-weight: 300; -} - -.filters { - margin: 0; - padding: 0; - list-style: none; - position: absolute; - right: 0; - left: 0; -} - -.filters li { - display: inline; -} - -.filters li a { - color: inherit; - margin: 3px; - padding: 3px 7px; - text-decoration: none; - border: 1px solid transparent; - border-radius: 3px; -} - -.filters li a.selected, -.filters li a:hover { - border-color: rgba(175, 47, 47, 0.1); -} - -.filters li a.selected { - border-color: rgba(175, 47, 47, 0.2); -} - -.clear-completed, -html .clear-completed:active { - float: right; - position: relative; - line-height: 20px; - text-decoration: none; - cursor: pointer; - position: relative; -} - -.clear-completed:hover { - text-decoration: underline; -} - -.info { - margin: 65px auto 0; - color: #bfbfbf; - font-size: 10px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-align: center; -} - -.info p { - line-height: 1; -} - -.info a { - color: inherit; - text-decoration: none; - font-weight: 400; -} - -.info a:hover { - text-decoration: underline; -} - -/* - Hack to remove background from Mobile Safari. - Can't use it globally since it destroys checkboxes in Firefox -*/ -@media screen and (-webkit-min-device-pixel-ratio:0) { - .toggle-all, - .todo-list li .toggle { - background: none; - } - - .todo-list li .toggle { - height: 40px; - } - - .toggle-all { - -webkit-transform: rotate(90deg); - transform: rotate(90deg); - -webkit-appearance: none; - appearance: none; - } -} - -@media (max-width: 430px) { - .footer { - height: 50px; - } - - .filters { - bottom: 10px; - } -} diff --git a/example/extending/evaluator/1-todo-webapp-report/resources/webapp/main.js b/example/extending/evaluator/1-todo-webapp-report/resources/webapp/main.js deleted file mode 100644 index 4a7617f50c2b..000000000000 --- a/example/extending/evaluator/1-todo-webapp-report/resources/webapp/main.js +++ /dev/null @@ -1,70 +0,0 @@ -var state = "all"; - -var todoApp = document.getElementsByClassName("todoapp")[0]; -function postFetchUpdate(url){ - fetch(url, { - method: "POST", - }) - .then(function(response){ return response.text()}) - .then(function (text) { - todoApp.innerHTML = text; - initListeners() - }) -} - -function bindEvent(cls, url, endState){ - - document.getElementsByClassName(cls)[0].addEventListener( - "mousedown", - function(evt){ - postFetchUpdate(url) - if (endState) state = endState - } - ); -} - -function bindIndexedEvent(cls, func){ - Array.from(document.getElementsByClassName(cls)).forEach( function(elem) { - elem.addEventListener( - "mousedown", - function(evt){ - postFetchUpdate(func(elem.getAttribute("data-todo-index"))) - } - ) - }); -} - -function initListeners(){ - bindIndexedEvent( - "destroy", - function(index){return "/delete/" + state + "/" + index} - ); - bindIndexedEvent( - "toggle", - function(index){return "/toggle/" + state + "/" + index} - ); - bindEvent("toggle-all", "/toggle-all/" + state); - bindEvent("todo-all", "/list/all", "all"); - bindEvent("todo-active", "/list/active", "active"); - bindEvent("todo-completed", "/list/completed", "completed"); - bindEvent("clear-completed", "/clear-completed/" + state); - var newTodoInput = document.getElementsByClassName("new-todo")[0]; - newTodoInput.addEventListener( - "keydown", - function(evt){ - if (evt.keyCode === 13) { - fetch("/add/" + state, { - method: "POST", - body: newTodoInput.value - }) - .then(function(response){ return response.text()}) - .then(function (text) { - newTodoInput.value = ""; - todoApp.innerHTML = text; - initListeners() - }) - } - } - ); -} -initListeners() \ No newline at end of file diff --git a/example/extending/evaluator/1-todo-webapp-report/src/WebApp.scala b/example/extending/evaluator/1-todo-webapp-report/src/WebApp.scala deleted file mode 100644 index 30787dcd037f..000000000000 --- a/example/extending/evaluator/1-todo-webapp-report/src/WebApp.scala +++ /dev/null @@ -1,137 +0,0 @@ -package webapp -import scalatags.Text.all._ -import scalatags.Text.tags2 - -object WebApp extends cask.MainRoutes { - case class Todo(checked: Boolean, text: String) - - object Todo { - implicit def todoRW: upickle.default.ReadWriter[Todo] = upickle.default.macroRW[Todo] - } - - var todos = Seq( - Todo(true, "Get started with Cask"), - Todo(false, "Profit!") - ) - - @cask.post("/list/:state") - def list(state: String) = renderBody(state) - - @cask.post("/add/:state") - def add(state: String, request: cask.Request) = { - todos = Seq(Todo(false, request.text())) ++ todos - renderBody(state) - } - - @cask.post("/delete/:state/:index") - def delete(state: String, index: Int) = { - todos = todos.patch(index, Nil, 1) - renderBody(state) - } - - @cask.post("/toggle/:state/:index") - def toggle(state: String, index: Int) = { - todos = todos.updated(index, todos(index).copy(checked = !todos(index).checked)) - renderBody(state) - } - - @cask.post("/clear-completed/:state") - def clearCompleted(state: String) = { - todos = todos.filter(!_.checked) - renderBody(state) - } - - @cask.post("/toggle-all/:state") - def toggleAll(state: String) = { - val next = todos.filter(_.checked).size != 0 - todos = todos.map(_.copy(checked = next)) - renderBody(state) - } - - def renderBody(state: String) /*: scalatags.Text.TypedTag[String] */ = { - val filteredTodos = state match { - case "all" => todos.zipWithIndex - case "active" => todos.zipWithIndex.filter(!_._1.checked) - case "completed" => todos.zipWithIndex.filter(_._1.checked) - } - div( - header( - cls := "header", - h1("todos"), - input(cls := "new-todo", placeholder := "What needs to be done?", autofocus := "") - ), - tags2.section( - cls := "main", - input( - id := "toggle-all", - cls := "toggle-all", - `type` := "checkbox", - if (todos.filter(_.checked).size != 0) checked else () - ), - label(`for` := "toggle-all", "Mark all as complete"), - ul( - cls := "todo-list", - for ((todo, index) <- filteredTodos) yield li( - if (todo.checked) cls := "completed" else (), - div( - cls := "view", - input( - cls := "toggle", - `type` := "checkbox", - if (todo.checked) checked else (), - data("todo-index") := index - ), - label(todo.text), - button(cls := "destroy", data("todo-index") := index) - ), - input(cls := "edit", value := todo.text) - ) - ) - ), - footer( - cls := "footer", - span(cls := "todo-count", strong(todos.filter(!_.checked).size), " items left"), - ul( - cls := "filters", - li(cls := "todo-all", a(if (state == "all") cls := "selected" else (), "All")), - li(cls := "todo-active", a(if (state == "active") cls := "selected" else (), "Active")), - li( - cls := "todo-completed", - a(if (state == "completed") cls := "selected" else (), "Completed") - ) - ), - button(cls := "clear-completed", "Clear completed") - ) - ) - } - - @cask.get("/") - def index() = { - doctype("html")( - html( - lang := "en", - head( - meta(charset := "utf-8"), - meta(name := "viewport", content := "width=device-width, initial-scale=1"), - tags2.title("Template • TodoMVC"), - link(rel := "stylesheet", href := "/static/index.css") - ), - body( - tags2.section(cls := "todoapp", renderBody("all")), - footer( - cls := "info", - p("Double-click to edit a todo"), - p("Created by ", a(href := "http://todomvc.com", "Li Haoyi")), - p("Part of ", a(href := "http://todomvc.com", "TodoMVC")) - ), - script(src := "/static/main.js") - ) - ) - ) - } - - @cask.staticResources("/static") - def static() = "webapp" - - initialize() -} diff --git a/example/extending/evaluator/1-todo-webapp-report/test/src/WebAppTests.scala b/example/extending/evaluator/1-todo-webapp-report/test/src/WebAppTests.scala deleted file mode 100644 index c17e542f7812..000000000000 --- a/example/extending/evaluator/1-todo-webapp-report/test/src/WebAppTests.scala +++ /dev/null @@ -1,24 +0,0 @@ -package webapp - -import utest._ - -object WebAppTests extends TestSuite { - def withServer[T](example: cask.main.Main)(f: String => T): T = { - val server = io.undertow.Undertow.builder - .addHttpListener(8181, "localhost") - .setHandler(example.defaultHandler) - .build - server.start() - val res = - try f("http://localhost:8181") - finally server.stop() - res - } - - val tests = Tests { - test("simpleRequest") - withServer(WebApp) { host => - val page = requests.get(host).text() - assert(page.contains("What needs to be done?")) - } - } -} diff --git a/example/extending/evaluator/2-resolve-tasks/build.mill b/example/extending/evaluator/2-resolve-tasks/build.mill new file mode 100644 index 000000000000..bf60858cc0cc --- /dev/null +++ b/example/extending/evaluator/2-resolve-tasks/build.mill @@ -0,0 +1,44 @@ +//// SNIPPET:BUILD + +package build +import mill.*, scalalib.* + +import mill.define._ +import mill.define.SelectMode +import mill.api.Result.create +import mill.api.Result + +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 customResolveTaskCommand(evaluator: Evaluator, tasks: String*) = + Task.Command(exclusive = true) { + val resolved = evaluator + .resolveTasks(tasks, SelectMode.Multi) + .get + + resolved.foreach { task => + println(s"Resolved task: ${task.label}") + } + () + } +} + +// This command resolves tasks and prints their labels + +/** Usage + +> ./mill customResolveTaskCommand compile test +Resolved task: compile +Resolved task: testForked + +*/ diff --git a/example/extending/evaluator/2-resolve-tasks/foo/src/Foo.scala b/example/extending/evaluator/2-resolve-tasks/foo/src/Foo.scala new file mode 100644 index 000000000000..2de577a0280f --- /dev/null +++ b/example/extending/evaluator/2-resolve-tasks/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/2-resolve-tasks/foo/test/src/FooTests.scala b/example/extending/evaluator/2-resolve-tasks/foo/test/src/FooTests.scala new file mode 100644 index 000000000000..9dcd8bf4e040 --- /dev/null +++ b/example/extending/evaluator/2-resolve-tasks/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/3-plan/build.mill b/example/extending/evaluator/3-plan/build.mill new file mode 100644 index 000000000000..435bde748b2e --- /dev/null +++ b/example/extending/evaluator/3-plan/build.mill @@ -0,0 +1,48 @@ +//// SNIPPET:BUILD + +package build +import mill.*, scalalib.* + +import mill.define._ +import mill.define.SelectMode +import mill.api.Result.create +import mill.api.Result + +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 customPlanCommand(evaluator: Evaluator, tasks: String*) = Task.Command(exclusive = true) { + val resolved = evaluator + .resolveTasks(tasks, SelectMode.Multi) + .get + + val plan = evaluator.plan(resolved) + .sortedGroups + .keys() + .map(_.toString) + .toArray + + plan.foreach(println) + () + } +} + +// This command plans the given tasks and prints the plan. +// It uses the `plan` method of the `Evaluator` to create a plan for the tasks and then prints the sorted groups of the plan. + +/** Usage + +> ./mill customPlanCommand compile +... +compile + +*/ diff --git a/example/extending/evaluator/3-plan/foo/src/Foo.scala b/example/extending/evaluator/3-plan/foo/src/Foo.scala new file mode 100644 index 000000000000..2de577a0280f --- /dev/null +++ b/example/extending/evaluator/3-plan/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/3-plan/foo/test/src/FooTests.scala b/example/extending/evaluator/3-plan/foo/test/src/FooTests.scala new file mode 100644 index 000000000000..9dcd8bf4e040 --- /dev/null +++ b/example/extending/evaluator/3-plan/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/4-execute/build.mill b/example/extending/evaluator/4-execute/build.mill new file mode 100644 index 000000000000..06ecdd282a17 --- /dev/null +++ b/example/extending/evaluator/4-execute/build.mill @@ -0,0 +1,44 @@ +//// SNIPPET:BUILD + +package build +import mill.*, scalalib.* + +import mill.define._ +import mill.define.SelectMode +import mill.api.Result.create +import mill.api.Result + +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 customExecuteCommand(evaluator: Evaluator, tasks: String*) = Task.Command(exclusive = true) { + val resolvedTasks = evaluator + .resolveTasks(tasks, SelectMode.Multi) + .get + + val executeRes = evaluator + .execute(resolvedTasks) + .executionResults + + println(s"Executed tasks result: ${executeRes.values}") + () + } +} +// This command executes the given tasks and prints the results of the execution. +// It uses the `execute` method of the `Evaluator` to run the tasks and then prints the results. + +/** Usage + +> ./mill customExecuteCommand assembly +.../assembly.dest/out.jar)) + +*/ diff --git a/example/extending/evaluator/4-execute/foo/src/Foo.scala b/example/extending/evaluator/4-execute/foo/src/Foo.scala new file mode 100644 index 000000000000..2de577a0280f --- /dev/null +++ b/example/extending/evaluator/4-execute/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/4-execute/foo/test/src/FooTests.scala b/example/extending/evaluator/4-execute/foo/test/src/FooTests.scala new file mode 100644 index 000000000000..9dcd8bf4e040 --- /dev/null +++ b/example/extending/evaluator/4-execute/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/5-evaluate/build.mill b/example/extending/evaluator/5-evaluate/build.mill new file mode 100644 index 000000000000..94bdecb608fc --- /dev/null +++ b/example/extending/evaluator/5-evaluate/build.mill @@ -0,0 +1,41 @@ +//// SNIPPET:BUILD + +package build +import mill.*, scalalib.* + +import mill.define._ +import mill.define.SelectMode +import mill.api.Result.create +import mill.api.Result + +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 customEvaluateCommand(evaluator: Evaluator, tasks: String*) = Task.Command(exclusive = true) { + val evalRes = evaluator + .evaluate(tasks, SelectMode.Multi) + .get + + println(s"Evaluated tasks values: ${evalRes.values}") + () + } +} + +// This command evaluates the given tasks and prints their values. +// It uses the `evaluate` method of the `Evaluator` to run the tasks and then prints the results. + +/** Usage + +> ./mill customEvaluateCommand assembly +.../assembly.dest/out.jar)) + +*/ diff --git a/example/extending/evaluator/5-evaluate/foo/src/Foo.scala b/example/extending/evaluator/5-evaluate/foo/src/Foo.scala new file mode 100644 index 000000000000..2de577a0280f --- /dev/null +++ b/example/extending/evaluator/5-evaluate/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/5-evaluate/foo/test/src/FooTests.scala b/example/extending/evaluator/5-evaluate/foo/test/src/FooTests.scala new file mode 100644 index 000000000000..9dcd8bf4e040 --- /dev/null +++ b/example/extending/evaluator/5-evaluate/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/website/docs/modules/ROOT/pages/extending/evaluator.adoc b/website/docs/modules/ROOT/pages/extending/evaluator.adoc index ae924fd9471e..3e641bf63428 100644 --- a/website/docs/modules/ROOT/pages/extending/evaluator.adoc +++ b/website/docs/modules/ROOT/pages/extending/evaluator.adoc @@ -1,5 +1,88 @@ -= Examples Documentation of usong Evaluator API Commands += Evaluator API Commands -== TodoMVC WebApp Build Report +== `resolveSegments` -include::partial$example/extending/evaluator/1-todo-webapp-report.adoc[] \ No newline at end of file +Resolves a sequence of Mill selector strings (such as task or module names) into a list of fully-qualified segment paths matching tasks or modules in the build. + +=== Parameters: +- `scriptArgs: Seq[String]` - The list of selector strings to resolve (e.g., `Seq("foo.bar")`). +- `selectMode: SelectMode` - Selection mode. Either `SelectMode.Multi` or `SelectMode.Separated` +- `allowPositionalCommandArgs: Boolean` (default: `false`) - Whether to allow positional command arguments. +- `resolveToModuleTasks: Boolean` (default: `false`) - Whether to resolve to module-level tasks if the selector points to a module. + +=== Returns: +- `mill.api.Result[List[Segments]]` - A result wrapping the list of resolved segment paths (`Segments`). + +=== Example Usage: + +include::partial$example/extending/evaluator/1-resolve-segments.adoc[] + + +== `resolveTasks` + +Resolves a sequence of selector strings into a list of concrete Mill tasks (`Task.Named`) matching those selectors. + +=== Parameters: +- `scriptArgs: Seq[String]` - Selector strings to resolve (e.g., `Seq("foo.assembly", "test.run")`) +- `selectMode: SelectMode` - Selection mode. Either `SelectMode.Multi` or `SelectMode.Separated` +- `allowPositionalCommandArgs: Boolean` (default: `false`) - Allow positional command arguments. +- `resolveToModuleTasks: Boolean` (default: `false`) - Resolve to module-level tasks if applicable. + +=== Returns +- `mill.api.Result[List[Task.Named[?]]]` - List of resolved named tasks. + +=== Example Usage: + +include::partial$example/extending/evaluator/2-resolve-tasks.adoc[] + + +== `plan` + +Computes and returns the execution plan (ordered task list) for a set of resolved tasks, without running them. + +=== Parameters: +- `tasks: Seq[Task.Named[?]]` - List of tasks to plan. + +=== Returns: +- `Plan` - A type with execution plan details (including all transitive tasks and their sorted grouping). + +=== Example Usage: + +include::partial$example/extending/evaluator/3-plan.adoc[] + + +== `execute` + +Executes a list of tasks and returns the results of their evaluation. + +=== Parameters: +- `targets: Seq[Task[T]]` - Tasks to execute. +- `reporter: Int` - It reports executions for targets specified in the `targets` parameter. +- `testReporter: TestReporter` (default: `TestReporter.DummyTestReporter`) - It reports executions for tests +- `logger: Logger` (default: `mill.api.Logger`) - Mill built-in logger to output build messages, task progress, errors and other information during execution. +- `serialCommandExec: Boolean` (default: `false`) - It execute targets one after another rather than in parallel if set to `true`. +- `selectiveExecution: Boolean` (default: `false`) - It enables selective execution mode, where only the tasks affected by recent changes( or their dependencies) are executed. + +=== Returns: +- `Evaluator.Result[T]` - Contains results of tasks executed, any watched files, and success/failure info for each task. + +=== Example Usage: + +include::partial$example/extending/evaluator/4-execute.adoc[] + + +== `evaluate` + +Evaluates tasks given as selector strings, resolving them first. + +=== Parameters: +- `scriptArgs: Seq[String]` - List of selector strings for tasks to evaluate. +- `selectMode: SelectMode` - Selection mode. Either `SelectMode.Multi` or `SelectMode.Separated` +- `selectiveExecution: Boolean` (default: `false`) - It enables selective execution mode, where only the tasks affected by recent changes( or their dependencies) are executed. + +=== Returns: +- `mill.api.Result[Evaluator.Result[Any]]` - The result of evaluating the tasks (including outputs, watched files, etc). + +=== Example Usage: + +include::partial$example/extending/evaluator/5-evaluate.adoc[] \ No newline at end of file From 0a98f6c1499604783d90c487a9f8a48b73d54f5c Mon Sep 17 00:00:00 2001 From: c0d33ngr Date: Mon, 2 Jun 2025 17:38:11 +0100 Subject: [PATCH 4/8] update doc --- .../evaluator/1-resolve-segments/build.mill | 2 +- .../extending/evaluator/4-execute/build.mill | 2 +- .../extending/evaluator/5-evaluate/build.mill | 2 +- .../ROOT/pages/extending/evaluator.adoc | 23 ++++++++++++++++++- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/example/extending/evaluator/1-resolve-segments/build.mill b/example/extending/evaluator/1-resolve-segments/build.mill index ea381d474016..9db2a8b51ae7 100644 --- a/example/extending/evaluator/1-resolve-segments/build.mill +++ b/example/extending/evaluator/1-resolve-segments/build.mill @@ -24,7 +24,7 @@ object `package` extends ScalaModule { .get segmentsResult.foreach { segment => - println(s"Segment: ${segment.parts.mkString(", ")}") + println(s"Segment: ${segment.render}") } () } diff --git a/example/extending/evaluator/4-execute/build.mill b/example/extending/evaluator/4-execute/build.mill index 06ecdd282a17..d9d2c5427bfb 100644 --- a/example/extending/evaluator/4-execute/build.mill +++ b/example/extending/evaluator/4-execute/build.mill @@ -39,6 +39,6 @@ object `package` extends ScalaModule { /** Usage > ./mill customExecuteCommand assembly -.../assembly.dest/out.jar)) +.../assembly.dest/out.jar... */ diff --git a/example/extending/evaluator/5-evaluate/build.mill b/example/extending/evaluator/5-evaluate/build.mill index 94bdecb608fc..77d7b981f6e1 100644 --- a/example/extending/evaluator/5-evaluate/build.mill +++ b/example/extending/evaluator/5-evaluate/build.mill @@ -36,6 +36,6 @@ object `package` extends ScalaModule { /** Usage > ./mill customEvaluateCommand assembly -.../assembly.dest/out.jar)) +.../assembly.dest/out.jar... */ diff --git a/website/docs/modules/ROOT/pages/extending/evaluator.adoc b/website/docs/modules/ROOT/pages/extending/evaluator.adoc index 3e641bf63428..399d034af17e 100644 --- a/website/docs/modules/ROOT/pages/extending/evaluator.adoc +++ b/website/docs/modules/ROOT/pages/extending/evaluator.adoc @@ -1,5 +1,8 @@ = 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. + == `resolveSegments` Resolves a sequence of Mill selector strings (such as task or module names) into a list of fully-qualified segment paths matching tasks or modules in the build. @@ -13,6 +16,10 @@ Resolves a sequence of Mill selector strings (such as task or module names) into === Returns: - `mill.api.Result[List[Segments]]` - A result wrapping the list of resolved segment paths (`Segments`). +=== Use Case: +Use this when you want to resolve a list of query selector into a list of `mill.api.Segments`. +`Segments` represent the "path" or selector for tasks or modules, but not the tasks themselves. + === Example Usage: include::partial$example/extending/evaluator/1-resolve-segments.adoc[] @@ -31,6 +38,10 @@ Resolves a sequence of selector strings into a list of concrete Mill tasks (`Tas === Returns - `mill.api.Result[List[Task.Named[?]]]` - List of resolved named tasks. +=== Use Case: +Use this when you want to resolve the same query selector strings into actual `Task.Named` instances. +These represent concrete tasks or modules that will be executed or inspected + === Example Usage: include::partial$example/extending/evaluator/2-resolve-tasks.adoc[] @@ -44,7 +55,10 @@ Computes and returns the execution plan (ordered task list) for a set of resolve - `tasks: Seq[Task.Named[?]]` - List of tasks to plan. === Returns: -- `Plan` - A type with execution plan details (including all transitive tasks and their sorted grouping). +- `Plan` - An object containing execution plan details (including all transitive tasks and their sorted grouping). + +=== Use Case: +Use this when you need to inspect the sequence of tasks Mill would execute for a given set of targets, without actually performing the execution. This is useful for debugging or understanding dependencies. === Example Usage: @@ -66,6 +80,10 @@ Executes a list of tasks and returns the results of their evaluation. === Returns: - `Evaluator.Result[T]` - Contains results of tasks executed, any watched files, and success/failure info for each task. +=== Use Case: +Choose `execute` when you needs to programmatically run one or more specific Mill tasks and you require access to their execution results, such as the output values, or success/failure status. +This is central for implementing conditional build logic, integrating with external tools, or collecting custom build metrics after tasks have completed. + === Example Usage: include::partial$example/extending/evaluator/4-execute.adoc[] @@ -83,6 +101,9 @@ Evaluates tasks given as selector strings, resolving them first. === Returns: - `mill.api.Result[Evaluator.Result[Any]]` - The result of evaluating the tasks (including outputs, watched files, etc). +=== Use Case: +Use this when you need to quickly evaluate tasks with a single command specified by selector strings without needing to explicitly resolve them first. + === Example Usage: include::partial$example/extending/evaluator/5-evaluate.adoc[] \ No newline at end of file From 0c87e595649fdf5b42b2151459e515adb0e50dcf Mon Sep 17 00:00:00 2001 From: c0d33ngr Date: Sat, 12 Jul 2025 04:20:21 +0100 Subject: [PATCH 5/8] WIP --- .../evaluator/1-resolve-segments/build.mill | 67 ++++++++--- .../evaluator/2-resolve-tasks/build.mill | 44 ------- .../2-resolve-tasks/foo/src/Foo.scala | 16 --- .../foo/test/src/FooTests.scala | 18 --- example/extending/evaluator/3-plan/build.mill | 48 -------- .../evaluator/3-plan/foo/src/Foo.scala | 16 --- .../3-plan/foo/test/src/FooTests.scala | 18 --- .../extending/evaluator/4-execute/build.mill | 44 ------- .../evaluator/4-execute/foo/src/Foo.scala | 16 --- .../4-execute/foo/test/src/FooTests.scala | 18 --- .../extending/evaluator/5-evaluate/build.mill | 41 ------- .../evaluator/5-evaluate/foo/src/Foo.scala | 16 --- .../5-evaluate/foo/test/src/FooTests.scala | 18 --- example/package.mill | 1 + .../ROOT/pages/extending/evaluator.adoc | 107 ++---------------- 15 files changed, 59 insertions(+), 429 deletions(-) delete mode 100644 example/extending/evaluator/2-resolve-tasks/build.mill delete mode 100644 example/extending/evaluator/2-resolve-tasks/foo/src/Foo.scala delete mode 100644 example/extending/evaluator/2-resolve-tasks/foo/test/src/FooTests.scala delete mode 100644 example/extending/evaluator/3-plan/build.mill delete mode 100644 example/extending/evaluator/3-plan/foo/src/Foo.scala delete mode 100644 example/extending/evaluator/3-plan/foo/test/src/FooTests.scala delete mode 100644 example/extending/evaluator/4-execute/build.mill delete mode 100644 example/extending/evaluator/4-execute/foo/src/Foo.scala delete mode 100644 example/extending/evaluator/4-execute/foo/test/src/FooTests.scala delete mode 100644 example/extending/evaluator/5-evaluate/build.mill delete mode 100644 example/extending/evaluator/5-evaluate/foo/src/Foo.scala delete mode 100644 example/extending/evaluator/5-evaluate/foo/test/src/FooTests.scala diff --git a/example/extending/evaluator/1-resolve-segments/build.mill b/example/extending/evaluator/1-resolve-segments/build.mill index 9db2a8b51ae7..7bada0009cdf 100644 --- a/example/extending/evaluator/1-resolve-segments/build.mill +++ b/example/extending/evaluator/1-resolve-segments/build.mill @@ -1,10 +1,8 @@ package build import mill.*, scalalib.* -import mill.define._ -import mill.define.SelectMode -import mill.api.Result.create -import mill.api.Result +import mill.api._ +import mill.api.daemon.SelectMode object `package` extends ScalaModule { def scalaVersion = "2.13.11" @@ -18,26 +16,61 @@ object `package` extends ScalaModule { def testFramework = "utest.runner.Framework" } - def customSegmentCommand(evaluator: Evaluator, tasks: String*) = Task.Command(exclusive = true) { - val segmentsResult = evaluator - .resolveSegments(tasks, SelectMode.Multi) - .get - - segmentsResult.foreach { segment => - println(s"Segment: ${segment.render}") + 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 resolves the segments of the given tasks and prints them. -// The segments are the parts of the task names that are used to group tasks. +// 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 customSegmentCommand compile test -Segment: compile -Segment: test +> ./mill depMapper +--- Dependency Users Report --- */ diff --git a/example/extending/evaluator/2-resolve-tasks/build.mill b/example/extending/evaluator/2-resolve-tasks/build.mill deleted file mode 100644 index bf60858cc0cc..000000000000 --- a/example/extending/evaluator/2-resolve-tasks/build.mill +++ /dev/null @@ -1,44 +0,0 @@ -//// SNIPPET:BUILD - -package build -import mill.*, scalalib.* - -import mill.define._ -import mill.define.SelectMode -import mill.api.Result.create -import mill.api.Result - -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 customResolveTaskCommand(evaluator: Evaluator, tasks: String*) = - Task.Command(exclusive = true) { - val resolved = evaluator - .resolveTasks(tasks, SelectMode.Multi) - .get - - resolved.foreach { task => - println(s"Resolved task: ${task.label}") - } - () - } -} - -// This command resolves tasks and prints their labels - -/** Usage - -> ./mill customResolveTaskCommand compile test -Resolved task: compile -Resolved task: testForked - -*/ diff --git a/example/extending/evaluator/2-resolve-tasks/foo/src/Foo.scala b/example/extending/evaluator/2-resolve-tasks/foo/src/Foo.scala deleted file mode 100644 index 2de577a0280f..000000000000 --- a/example/extending/evaluator/2-resolve-tasks/foo/src/Foo.scala +++ /dev/null @@ -1,16 +0,0 @@ -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-resolve-tasks/foo/test/src/FooTests.scala b/example/extending/evaluator/2-resolve-tasks/foo/test/src/FooTests.scala deleted file mode 100644 index 9dcd8bf4e040..000000000000 --- a/example/extending/evaluator/2-resolve-tasks/foo/test/src/FooTests.scala +++ /dev/null @@ -1,18 +0,0 @@ -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/3-plan/build.mill b/example/extending/evaluator/3-plan/build.mill deleted file mode 100644 index 435bde748b2e..000000000000 --- a/example/extending/evaluator/3-plan/build.mill +++ /dev/null @@ -1,48 +0,0 @@ -//// SNIPPET:BUILD - -package build -import mill.*, scalalib.* - -import mill.define._ -import mill.define.SelectMode -import mill.api.Result.create -import mill.api.Result - -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 customPlanCommand(evaluator: Evaluator, tasks: String*) = Task.Command(exclusive = true) { - val resolved = evaluator - .resolveTasks(tasks, SelectMode.Multi) - .get - - val plan = evaluator.plan(resolved) - .sortedGroups - .keys() - .map(_.toString) - .toArray - - plan.foreach(println) - () - } -} - -// This command plans the given tasks and prints the plan. -// It uses the `plan` method of the `Evaluator` to create a plan for the tasks and then prints the sorted groups of the plan. - -/** Usage - -> ./mill customPlanCommand compile -... -compile - -*/ diff --git a/example/extending/evaluator/3-plan/foo/src/Foo.scala b/example/extending/evaluator/3-plan/foo/src/Foo.scala deleted file mode 100644 index 2de577a0280f..000000000000 --- a/example/extending/evaluator/3-plan/foo/src/Foo.scala +++ /dev/null @@ -1,16 +0,0 @@ -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/3-plan/foo/test/src/FooTests.scala b/example/extending/evaluator/3-plan/foo/test/src/FooTests.scala deleted file mode 100644 index 9dcd8bf4e040..000000000000 --- a/example/extending/evaluator/3-plan/foo/test/src/FooTests.scala +++ /dev/null @@ -1,18 +0,0 @@ -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/4-execute/build.mill b/example/extending/evaluator/4-execute/build.mill deleted file mode 100644 index d9d2c5427bfb..000000000000 --- a/example/extending/evaluator/4-execute/build.mill +++ /dev/null @@ -1,44 +0,0 @@ -//// SNIPPET:BUILD - -package build -import mill.*, scalalib.* - -import mill.define._ -import mill.define.SelectMode -import mill.api.Result.create -import mill.api.Result - -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 customExecuteCommand(evaluator: Evaluator, tasks: String*) = Task.Command(exclusive = true) { - val resolvedTasks = evaluator - .resolveTasks(tasks, SelectMode.Multi) - .get - - val executeRes = evaluator - .execute(resolvedTasks) - .executionResults - - println(s"Executed tasks result: ${executeRes.values}") - () - } -} -// This command executes the given tasks and prints the results of the execution. -// It uses the `execute` method of the `Evaluator` to run the tasks and then prints the results. - -/** Usage - -> ./mill customExecuteCommand assembly -.../assembly.dest/out.jar... - -*/ diff --git a/example/extending/evaluator/4-execute/foo/src/Foo.scala b/example/extending/evaluator/4-execute/foo/src/Foo.scala deleted file mode 100644 index 2de577a0280f..000000000000 --- a/example/extending/evaluator/4-execute/foo/src/Foo.scala +++ /dev/null @@ -1,16 +0,0 @@ -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/4-execute/foo/test/src/FooTests.scala b/example/extending/evaluator/4-execute/foo/test/src/FooTests.scala deleted file mode 100644 index 9dcd8bf4e040..000000000000 --- a/example/extending/evaluator/4-execute/foo/test/src/FooTests.scala +++ /dev/null @@ -1,18 +0,0 @@ -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/5-evaluate/build.mill b/example/extending/evaluator/5-evaluate/build.mill deleted file mode 100644 index 77d7b981f6e1..000000000000 --- a/example/extending/evaluator/5-evaluate/build.mill +++ /dev/null @@ -1,41 +0,0 @@ -//// SNIPPET:BUILD - -package build -import mill.*, scalalib.* - -import mill.define._ -import mill.define.SelectMode -import mill.api.Result.create -import mill.api.Result - -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 customEvaluateCommand(evaluator: Evaluator, tasks: String*) = Task.Command(exclusive = true) { - val evalRes = evaluator - .evaluate(tasks, SelectMode.Multi) - .get - - println(s"Evaluated tasks values: ${evalRes.values}") - () - } -} - -// This command evaluates the given tasks and prints their values. -// It uses the `evaluate` method of the `Evaluator` to run the tasks and then prints the results. - -/** Usage - -> ./mill customEvaluateCommand assembly -.../assembly.dest/out.jar... - -*/ diff --git a/example/extending/evaluator/5-evaluate/foo/src/Foo.scala b/example/extending/evaluator/5-evaluate/foo/src/Foo.scala deleted file mode 100644 index 2de577a0280f..000000000000 --- a/example/extending/evaluator/5-evaluate/foo/src/Foo.scala +++ /dev/null @@ -1,16 +0,0 @@ -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/5-evaluate/foo/test/src/FooTests.scala b/example/extending/evaluator/5-evaluate/foo/test/src/FooTests.scala deleted file mode 100644 index 9dcd8bf4e040..000000000000 --- a/example/extending/evaluator/5-evaluate/foo/test/src/FooTests.scala +++ /dev/null @@ -1,18 +0,0 @@ -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/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/pages/extending/evaluator.adoc b/website/docs/modules/ROOT/pages/extending/evaluator.adoc index 399d034af17e..53a7ef240641 100644 --- a/website/docs/modules/ROOT/pages/extending/evaluator.adoc +++ b/website/docs/modules/ROOT/pages/extending/evaluator.adoc @@ -3,107 +3,16 @@ 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. -== `resolveSegments` +== Dependency Mapper -Resolves a sequence of Mill selector strings (such as task or module names) into a list of fully-qualified segment paths matching tasks or modules in the build. +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`. -=== Parameters: -- `scriptArgs: Seq[String]` - The list of selector strings to resolve (e.g., `Seq("foo.bar")`). -- `selectMode: SelectMode` - Selection mode. Either `SelectMode.Multi` or `SelectMode.Separated` -- `allowPositionalCommandArgs: Boolean` (default: `false`) - Whether to allow positional command arguments. -- `resolveToModuleTasks: Boolean` (default: `false`) - Whether to resolve to module-level tasks if the selector points to a module. +This task: -=== Returns: -- `mill.api.Result[List[Segments]]` - A result wrapping the list of resolved segment paths (`Segments`). - -=== Use Case: -Use this when you want to resolve a list of query selector into a list of `mill.api.Segments`. -`Segments` represent the "path" or selector for tasks or modules, but not the tasks themselves. - -=== Example Usage: +* 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-resolve-segments.adoc[] - - -== `resolveTasks` - -Resolves a sequence of selector strings into a list of concrete Mill tasks (`Task.Named`) matching those selectors. - -=== Parameters: -- `scriptArgs: Seq[String]` - Selector strings to resolve (e.g., `Seq("foo.assembly", "test.run")`) -- `selectMode: SelectMode` - Selection mode. Either `SelectMode.Multi` or `SelectMode.Separated` -- `allowPositionalCommandArgs: Boolean` (default: `false`) - Allow positional command arguments. -- `resolveToModuleTasks: Boolean` (default: `false`) - Resolve to module-level tasks if applicable. - -=== Returns -- `mill.api.Result[List[Task.Named[?]]]` - List of resolved named tasks. - -=== Use Case: -Use this when you want to resolve the same query selector strings into actual `Task.Named` instances. -These represent concrete tasks or modules that will be executed or inspected - -=== Example Usage: - -include::partial$example/extending/evaluator/2-resolve-tasks.adoc[] - - -== `plan` - -Computes and returns the execution plan (ordered task list) for a set of resolved tasks, without running them. - -=== Parameters: -- `tasks: Seq[Task.Named[?]]` - List of tasks to plan. - -=== Returns: -- `Plan` - An object containing execution plan details (including all transitive tasks and their sorted grouping). - -=== Use Case: -Use this when you need to inspect the sequence of tasks Mill would execute for a given set of targets, without actually performing the execution. This is useful for debugging or understanding dependencies. - -=== Example Usage: - -include::partial$example/extending/evaluator/3-plan.adoc[] - - -== `execute` - -Executes a list of tasks and returns the results of their evaluation. - -=== Parameters: -- `targets: Seq[Task[T]]` - Tasks to execute. -- `reporter: Int` - It reports executions for targets specified in the `targets` parameter. -- `testReporter: TestReporter` (default: `TestReporter.DummyTestReporter`) - It reports executions for tests -- `logger: Logger` (default: `mill.api.Logger`) - Mill built-in logger to output build messages, task progress, errors and other information during execution. -- `serialCommandExec: Boolean` (default: `false`) - It execute targets one after another rather than in parallel if set to `true`. -- `selectiveExecution: Boolean` (default: `false`) - It enables selective execution mode, where only the tasks affected by recent changes( or their dependencies) are executed. - -=== Returns: -- `Evaluator.Result[T]` - Contains results of tasks executed, any watched files, and success/failure info for each task. - -=== Use Case: -Choose `execute` when you needs to programmatically run one or more specific Mill tasks and you require access to their execution results, such as the output values, or success/failure status. -This is central for implementing conditional build logic, integrating with external tools, or collecting custom build metrics after tasks have completed. - -=== Example Usage: - -include::partial$example/extending/evaluator/4-execute.adoc[] - - -== `evaluate` - -Evaluates tasks given as selector strings, resolving them first. - -=== Parameters: -- `scriptArgs: Seq[String]` - List of selector strings for tasks to evaluate. -- `selectMode: SelectMode` - Selection mode. Either `SelectMode.Multi` or `SelectMode.Separated` -- `selectiveExecution: Boolean` (default: `false`) - It enables selective execution mode, where only the tasks affected by recent changes( or their dependencies) are executed. - -=== Returns: -- `mill.api.Result[Evaluator.Result[Any]]` - The result of evaluating the tasks (including outputs, watched files, etc). - -=== Use Case: -Use this when you need to quickly evaluate tasks with a single command specified by selector strings without needing to explicitly resolve them first. - -=== Example Usage: - -include::partial$example/extending/evaluator/5-evaluate.adoc[] \ No newline at end of file From e5a24a3da43eeb9d7ebe51c9c8edf1f957766b52 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 12 Jul 2025 03:22:27 +0000 Subject: [PATCH 6/8] [autofix.ci] apply automated fixes --- .../evaluator/1-resolve-segments/build.mill | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/example/extending/evaluator/1-resolve-segments/build.mill b/example/extending/evaluator/1-resolve-segments/build.mill index 7bada0009cdf..91622564ea62 100644 --- a/example/extending/evaluator/1-resolve-segments/build.mill +++ b/example/extending/evaluator/1-resolve-segments/build.mill @@ -20,13 +20,13 @@ object `package` extends ScalaModule { 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}" @@ -40,12 +40,15 @@ object `package` extends ScalaModule { } } println("--- End Report ---") - case failure => + case failure => println(s"Task execution failed: $failure") } } - def findDependencyUsage(dep: mill.javalib.Dep, sourceFiles: Seq[os.Path]): Seq[(os.Path, Seq[String])] = { + 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) @@ -55,7 +58,7 @@ object `package` extends ScalaModule { 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)) From 81068e6416787fab88e047c790cc3447189f8ed6 Mon Sep 17 00:00:00 2001 From: c0d33ngr Date: Tue, 15 Jul 2025 14:51:17 +0100 Subject: [PATCH 7/8] WIP --- .../build.mill | 0 .../foo/src/Foo.scala | 0 .../foo/test/src/FooTests.scala | 0 .../2-unreferencedfiles/bar/src/Foo.scala | 16 ++++ .../bar/test/src/FooTests.scala | 18 ++++ .../evaluator/2-unreferencedfiles/build.mill | 90 +++++++++++++++++++ .../ROOT/pages/extending/evaluator.adoc | 18 +++- 7 files changed, 140 insertions(+), 2 deletions(-) rename example/extending/evaluator/{1-resolve-segments => 1-depmapper}/build.mill (100%) rename example/extending/evaluator/{1-resolve-segments => 1-depmapper}/foo/src/Foo.scala (100%) rename example/extending/evaluator/{1-resolve-segments => 1-depmapper}/foo/test/src/FooTests.scala (100%) create mode 100644 example/extending/evaluator/2-unreferencedfiles/bar/src/Foo.scala create mode 100644 example/extending/evaluator/2-unreferencedfiles/bar/test/src/FooTests.scala create mode 100644 example/extending/evaluator/2-unreferencedfiles/build.mill diff --git a/example/extending/evaluator/1-resolve-segments/build.mill b/example/extending/evaluator/1-depmapper/build.mill similarity index 100% rename from example/extending/evaluator/1-resolve-segments/build.mill rename to example/extending/evaluator/1-depmapper/build.mill diff --git a/example/extending/evaluator/1-resolve-segments/foo/src/Foo.scala b/example/extending/evaluator/1-depmapper/foo/src/Foo.scala similarity index 100% rename from example/extending/evaluator/1-resolve-segments/foo/src/Foo.scala rename to example/extending/evaluator/1-depmapper/foo/src/Foo.scala diff --git a/example/extending/evaluator/1-resolve-segments/foo/test/src/FooTests.scala b/example/extending/evaluator/1-depmapper/foo/test/src/FooTests.scala similarity index 100% rename from example/extending/evaluator/1-resolve-segments/foo/test/src/FooTests.scala rename to example/extending/evaluator/1-depmapper/foo/test/src/FooTests.scala 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..b67db7b44ef8 --- /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/website/docs/modules/ROOT/pages/extending/evaluator.adoc b/website/docs/modules/ROOT/pages/extending/evaluator.adoc index 53a7ef240641..0cb3bea64942 100644 --- a/website/docs/modules/ROOT/pages/extending/evaluator.adoc +++ b/website/docs/modules/ROOT/pages/extending/evaluator.adoc @@ -1,4 +1,4 @@ -= Evaluator API Commands += 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. @@ -15,4 +15,18 @@ This task: * 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-resolve-segments.adoc[] +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[] + From a439f2d664568838be96f91608f4d179c4f16540 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:00:08 +0000 Subject: [PATCH 8/8] [autofix.ci] apply automated fixes --- .../evaluator/2-unreferencedfiles/build.mill | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/example/extending/evaluator/2-unreferencedfiles/build.mill b/example/extending/evaluator/2-unreferencedfiles/build.mill index b67db7b44ef8..5f0699579414 100644 --- a/example/extending/evaluator/2-unreferencedfiles/build.mill +++ b/example/extending/evaluator/2-unreferencedfiles/build.mill @@ -22,7 +22,7 @@ object `package` extends ScalaModule { segmentResult.foreach { segment => println(s"Planning segment: ${segment.render}") } - + val resolveResult = evaluator.resolveTasks(sourceFiles, SelectMode.Multi).get val plan = evaluator.plan(resolveResult) .sortedGroups @@ -30,9 +30,9 @@ object `package` extends ScalaModule { .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 { @@ -43,15 +43,15 @@ object `package` extends ScalaModule { 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") @@ -60,7 +60,7 @@ object `package` extends ScalaModule { .filter(!_.segments.contains(".git")) .filter(!_.segments.contains("out")) .toSet - + // Find unreferenced files val unreferenced = diskSources -- knownSources if (unreferenced.nonEmpty) {