From 025a6ed4c104fe751ae4237054eab2e11eb8abf1 Mon Sep 17 00:00:00 2001 From: Armin Motekalem Date: Fri, 5 Sep 2025 07:57:30 -0700 Subject: [PATCH 1/3] document-more-complex-task-usecases --- .../tasks/3-anonymous-tasks/build.mill | 3 + .../8-inheritance-and-chaining/build.mill | 63 +++++++++++++++++++ .../ROOT/pages/fundamentals/tasks.adoc | 4 ++ 3 files changed, 70 insertions(+) create mode 100644 example/fundamentals/tasks/8-inheritance-and-chaining/build.mill diff --git a/example/fundamentals/tasks/3-anonymous-tasks/build.mill b/example/fundamentals/tasks/3-anonymous-tasks/build.mill index 0451d79ebabd..c77e58c33d80 100644 --- a/example/fundamentals/tasks/3-anonymous-tasks/build.mill +++ b/example/fundamentals/tasks/3-anonymous-tasks/build.mill @@ -23,6 +23,9 @@ def printFileData(fileName: String) = Task.Command { // anywhere and passed around any way you want, until you finally make use of them // within a downstream task or command. // +// Anonymous tasks work well when parameters are known at task definition time. Dynamically +// generated inputs can not be used with Anonymous Tasks, as this would break caching. +// // While an anonymous task ``foo``'s own output is not cached, if it is used in a // downstream task `baz` and the upstream task `bar` hasn't changed, // ``baz``'s cached output will be used and ``foo``'s evaluation will be skipped diff --git a/example/fundamentals/tasks/8-inheritance-and-chaining/build.mill b/example/fundamentals/tasks/8-inheritance-and-chaining/build.mill new file mode 100644 index 000000000000..54feee4fd14a --- /dev/null +++ b/example/fundamentals/tasks/8-inheritance-and-chaining/build.mill @@ -0,0 +1,63 @@ +package build +import mill.* +import mill.api.TaskCtx + +trait BaseModule extends Module { + def generate = Task { + val output = Task.dest / "generated.txt" + os.write(output, "hello") + PathRef(output) + } +} + +// This module uses the base implementation unchanged +// Calling generate here will create: +// out/moduleA/generate.dest/generated.txt +object moduleA extends BaseModule + + +// When overriding tasks, never modify the parent task's output. +// Instead, create new output in your own task's destination folder +// Here we will generate two files in the output: +// out/moduleB/generate.super/BaseModule.dest/generated.txt (parent output) +// out/moduleB/generate.dest/processed.txt (this module's output) +// Note: We can not over write the original contents of +// generated.txt, because it is in a different output destination +object moduleB extends BaseModule { + override def generate = Task { + val parentResult = super.generate() + val parentContent = os.read(parentResult.path) + val processed = parentContent + .replaceAll("hello", "world") + .trim + + writeData(processed, "processed.txt") + } + + // Helper function for writing data within a task context + // Note: This is a regular function, not a Task, so it can accept + // runtime parameters. It uses implicit TaskCtx to access Task.dest + private def writeData(data: String, name: String)(implicit ctx: TaskCtx): PathRef = { + val outputPath = ctx.dest / name + os.write(outputPath, data) + PathRef(outputPath) + } +} + +// We can also acheive similar results by chaining tasks together. +object moduleC extends BaseModule { + override def generate = Task{ + processGenerated() + } + + def processGenerated = Task { + val parentResult = super.generate() + val parentContent = os.read(parentResult.path) + val processed = parentContent + .replaceAll("hello", "world") + .trim + val outputPath = Task.dest / "processed.txt" + os.write(outputPath, processed) + PathRef(outputPath) + } +} \ No newline at end of file diff --git a/website/docs/modules/ROOT/pages/fundamentals/tasks.adoc b/website/docs/modules/ROOT/pages/fundamentals/tasks.adoc index b12f332297ef..28f72d93c1e1 100644 --- a/website/docs/modules/ROOT/pages/fundamentals/tasks.adoc +++ b/website/docs/modules/ROOT/pages/fundamentals/tasks.adoc @@ -52,3 +52,7 @@ include::partial$example/fundamentals/tasks/6-workers.adoc[] include::partial$example/fundamentals/tasks/3-anonymous-tasks.adoc[] +=== Inheritance and Chaining of Tasks + +include::partial$example/fundamentals/tasks/8-inheritance-and-chaining.adoc[] + From 2955cfadfdcca17d4dbc060085e41677308e22e0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:06:01 +0000 Subject: [PATCH 2/3] [autofix.ci] apply automated fixes --- .../fundamentals/tasks/8-inheritance-and-chaining/build.mill | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/example/fundamentals/tasks/8-inheritance-and-chaining/build.mill b/example/fundamentals/tasks/8-inheritance-and-chaining/build.mill index 54feee4fd14a..bc6073c865ed 100644 --- a/example/fundamentals/tasks/8-inheritance-and-chaining/build.mill +++ b/example/fundamentals/tasks/8-inheritance-and-chaining/build.mill @@ -15,7 +15,6 @@ trait BaseModule extends Module { // out/moduleA/generate.dest/generated.txt object moduleA extends BaseModule - // When overriding tasks, never modify the parent task's output. // Instead, create new output in your own task's destination folder // Here we will generate two files in the output: @@ -46,7 +45,7 @@ object moduleB extends BaseModule { // We can also acheive similar results by chaining tasks together. object moduleC extends BaseModule { - override def generate = Task{ + override def generate = Task { processGenerated() } @@ -60,4 +59,4 @@ object moduleC extends BaseModule { os.write(outputPath, processed) PathRef(outputPath) } -} \ No newline at end of file +} From 39b82b39f4104d4154ab3c1f4f942fef862a678d Mon Sep 17 00:00:00 2001 From: Armin Motekalem Date: Mon, 8 Sep 2025 07:34:50 -0700 Subject: [PATCH 3/3] fix-anon-task-input-parameter-documentation --- .../fundamentals/tasks/3-anonymous-tasks/build.mill | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/example/fundamentals/tasks/3-anonymous-tasks/build.mill b/example/fundamentals/tasks/3-anonymous-tasks/build.mill index c77e58c33d80..9d95f3f825d3 100644 --- a/example/fundamentals/tasks/3-anonymous-tasks/build.mill +++ b/example/fundamentals/tasks/3-anonymous-tasks/build.mill @@ -24,7 +24,15 @@ def printFileData(fileName: String) = Task.Command { // within a downstream task or command. // // Anonymous tasks work well when parameters are known at task definition time. Dynamically -// generated inputs can not be used with Anonymous Tasks, as this would break caching. +// generated inputs can not be used with Anonymous Tasks, since mill wouldn't be able to build +// the dependency tree. We need to know the parameters before creating the task, but the tasks +// need to be created before we can start evaluation, we have a classic hen-or-egg problem. +// Common strategies around this are: +// 1. Define the values outside of a task, so Mill knows how to create the anonymous +// tasks before evaluation starts. +// 2. Put the value itself into a task, so it can be properly ordered to be evaluated +// before it's dependencies. +// 3. Make it not a task but a normal def // // While an anonymous task ``foo``'s own output is not cached, if it is used in a // downstream task `baz` and the upstream task `bar` hasn't changed,