Skip to content

第 5 章 Task 全攻略:从 Hello 到企业级自定义 Task

学习目标:会写各种 Task —— 简单的、Typed 的、自定义类的;能让自定义 Task 支持增量构建(这是 Gradle 性能的核心);理解 inputs / outputs / @TaskAction 三大注解;知道何时该用 doLast,何时该写专门的 Task 类。


5.1 Task 是什么的回顾

第 3 章已经讲过 Task 的基本概念,本章深入讲"怎么写好一个 Task"。

每个 Task 的"完整画像":

   ┌──────────────────────────────────────────────────────────┐
   │  Task                                                     │
   ├──────────────────────────────────────────────────────────┤
   │  ① name                — 在 Project 内唯一                  │
   │  ② group + description — 在 ./gradlew tasks 列表里显示       │
   │  ③ dependsOn / mustRunAfter / finalizedBy — 跟其他 Task 关系 │
   │  ④ inputs              — 这个 Task 看哪些数据                │
   │  ⑤ outputs             — 这个 Task 产出什么                  │
   │  ⑥ actions (doFirst/doLast/@TaskAction) — 真正干活的代码     │
   │  ⑦ enabled / onlyIf    — 控制是否执行                       │
   └──────────────────────────────────────────────────────────┘

   ★ ④⑤ 是增量构建 / 构建缓存的基石:声明清楚了才会被跳过

5.2 三种"写 Task"的方式

按从简到繁排:

5.2.1 方式 1:临时 Task(doLast 块)

适合快速一次性脚本

kotlin
tasks.register("printVersion") {
    doLast {
        println("当前版本:${project.version}")
    }
}

缺点:没有 input/output,每次都全量执行。不能复用。

5.2.2 方式 2:Typed Task(用 Gradle 内置类型)

适合用现成的 Task 类型组装

kotlin
tasks.register<Copy>("copyConfig") {
    from("src/main/resources/config")
    into(layout.buildDirectory.dir("config"))
    include("**/*.yml")
}

tasks.register<Zip>("releaseZip") {
    archiveFileName = "release-${project.version}.zip"
    destinationDirectory = layout.buildDirectory.dir("dist")
    from(layout.buildDirectory.dir("classes"))
}

tasks.register<Exec>("dockerBuild") {
    commandLine("docker", "build", "-t", "myapp:${project.version}", ".")
}

优点:内置类型已经声明了 input/output,自动支持增量构建

5.2.3 方式 3:自定义 Task 类

适合复杂、要复用、要有完整生命周期的 Task

kotlin
// 一个统计代码行数的 Task
abstract class CountLinesTask : DefaultTask() {

    @get:InputDirectory
    abstract val sourceDir: DirectoryProperty

    @get:Input
    abstract val fileExtension: Property<String>

    @get:OutputFile
    abstract val report: RegularFileProperty

    @TaskAction
    fun count() {
        val pattern = "**/*.${fileExtension.get()}"
        val files = sourceDir.get().asFileTree.matching { include(pattern) }.files
        val total = files.sumOf { it.readLines().size }

        report.get().asFile.writeText(
            "Files: ${files.size}\nTotal lines: $total\n"
        )
        println("✅ Counted $total lines in ${files.size} files → ${report.get().asFile}")
    }
}

// 注册并配置
tasks.register<CountLinesTask>("countJavaLines") {
    sourceDir = layout.projectDirectory.dir("src/main/java")
    fileExtension = "java"
    report = layout.buildDirectory.file("reports/lines.txt")
}

最重要:这种写法 自动支持增量构建 —— sourceDir 没变就跳过,跑出 UP-TO-DATE


5.3 增量构建:Gradle 性能的核心

5.3.1 什么是增量构建?

第 1 章讲过:没改的就别重跑。Gradle 用 inputs / outputs 的 hash 指纹决定是否跳过。

   第一次跑 myTask:
     input hash:  A1B2C3
     output hash: X9Y8Z7
     → 实际执行 → 写入 .gradle/ 缓存

   第二次跑 myTask:
     input hash:  A1B2C3   ← 没变
     output hash: X9Y8Z7   ← 也没变
     → 命中缓存 → UP-TO-DATE → 跳过!

   第三次跑(你改了源码):
     input hash:  A1B2C3   ← 变成 D4E5F6
     → 跟缓存对不上 → 重新执行

5.3.2 怎么声明 inputs / outputs

方式 A:临时 Task 用 inputs/outputs API

kotlin
tasks.register("processData") {
    inputs.file("data/input.csv")
    inputs.dir("data/templates")
    inputs.property("environment", project.property("env"))

    outputs.file(layout.buildDirectory.file("output.json"))
    outputs.dir(layout.buildDirectory.dir("processed"))

    doLast {
        // 真正干活
    }
}

方式 B:自定义 Task 类用注解强烈推荐):

kotlin
abstract class ProcessDataTask : DefaultTask() {
    @get:InputFile
    abstract val inputCsv: RegularFileProperty

    @get:InputDirectory
    abstract val templatesDir: DirectoryProperty

    @get:Input
    abstract val environment: Property<String>

    @get:OutputFile
    abstract val outputJson: RegularFileProperty

    @TaskAction
    fun process() {
        // 真正干活
    }
}

5.3.3 输入输出注解大全

注解作用类型
@Input标量输入(字符串、数字、boolean)Property<T>
@InputFile单个文件输入RegularFileProperty
@InputDirectory目录输入(递归)DirectoryProperty
@InputFiles多个文件输入ConfigurableFileCollection
@OutputFile单个文件输出RegularFileProperty
@OutputDirectory目录输出DirectoryProperty
@OutputFiles多个文件输出ConfigurableFileCollection
@Optional标记为可选(不强制要求)/
@Incremental增量输入(只处理变化的文件)InputChanges
@CacheableTask标记此 Task 支持构建缓存(类级别注解)

5.3.4 真实场景:把任务从"全量"改造成"增量"

改造前(每次都重跑):

kotlin
tasks.register("renderTemplates") {
    doLast {
        val templates = file("templates").listFiles().orEmpty()
        templates.forEach { tpl ->
            val rendered = tpl.readText().replace("{{name}}", "World")
            file("build/rendered/${tpl.name}").also {
                it.parentFile.mkdirs()
            }.writeText(rendered)
        }
    }
}

// 跑两次:
// 第一次:执行(5s)
// 第二次:还是执行(5s)  ← 浪费!

改造后(增量友好):

kotlin
tasks.register("renderTemplates") {
    inputs.dir("templates")
    inputs.property("name", "World")
    outputs.dir(layout.buildDirectory.dir("rendered"))

    doLast {
        val templates = file("templates").listFiles().orEmpty()
        templates.forEach { tpl ->
            val rendered = tpl.readText().replace("{{name}}", "World")
            layout.buildDirectory.file("rendered/${tpl.name}").get().asFile
                .also { it.parentFile.mkdirs() }
                .writeText(rendered)
        }
    }
}

// 跑两次:
// 第一次:执行(5s)
// 第二次:UP-TO-DATE  ← 0.1s 完成!

📌 生活化类比:增量构建像"周末妈妈做家务"。第一次大扫除(input = 房间状态)→ 写满 100 个清单(output)。下周再来:发现房间没变(input hash 一致),就不用重做了。


5.4 enabled vs onlyIf:控制 Task 是否执行

5.4.1 enabled = false(直接禁用)

kotlin
tasks.register("foo") {
    enabled = false
    doLast { println("永远不会跑") }
}

./gradlew foo

> Task :foo SKIPPED

5.4.2 onlyIf {}(条件执行)

kotlin
tasks.register("uploadToS3") {
    onlyIf {
        project.hasProperty("release")
    }
    doLast { println("Uploading...") }
}

跑:

bash
$ ./gradlew uploadToS3
> Task :uploadToS3 SKIPPED       ← 条件不满足,跳过

$ ./gradlew uploadToS3 -Prelease
> Task :uploadToS3
Uploading... 条件满足,执行

💡 实战:onlyIfenabled 更灵活,能在配置阶段读环境变量、project property 等。


5.5 几种典型自定义 Task 完整示例

5.5.1 例 1:发版前自动更新版本号

kotlin
abstract class BumpVersionTask : DefaultTask() {
    @get:InputFile
    abstract val versionFile: RegularFileProperty

    @get:Input
    @get:Optional
    abstract val newVersion: Property<String>

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun bump() {
        val current = versionFile.get().asFile.readText().trim()
        val next = newVersion.orNull ?: nextPatch(current)
        outputFile.get().asFile.writeText(next)
        println("📦 Version: $current$next")
    }

    private fun nextPatch(v: String): String {
        val parts = v.split(".").map { it.toInt() }.toMutableList()
        parts[2] = parts[2] + 1
        return parts.joinToString(".")
    }
}

tasks.register<BumpVersionTask>("bumpVersion") {
    versionFile = layout.projectDirectory.file("VERSION")
    outputFile = layout.projectDirectory.file("VERSION")
    newVersion = providers.gradleProperty("v").orNull
}

// 跑:./gradlew bumpVersion           → 自动 patch+1
// 跑:./gradlew bumpVersion -Pv=2.0.0 → 强制改成 2.0.0

5.5.2 例 2:编译前自动生成 BuildInfo 类

kotlin
abstract class GenerateBuildInfoTask : DefaultTask() {
    @get:Input
    abstract val packageName: Property<String>

    @get:Input
    abstract val version: Property<String>

    @get:Input
    abstract val buildTime: Property<String>

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @TaskAction
    fun generate() {
        val pkg = packageName.get()
        val pkgPath = pkg.replace('.', '/')
        val target = outputDir.get().asFile.resolve("$pkgPath/BuildInfo.java")
        target.parentFile.mkdirs()

        target.writeText("""
            package $pkg;
            public final class BuildInfo {
                public static final String VERSION = "${version.get()}";
                public static final String BUILD_TIME = "${buildTime.get()}";
                private BuildInfo() {}
            }
        """.trimIndent())

        println("📝 Generated: ${target.absolutePath}")
    }
}

tasks.register<GenerateBuildInfoTask>("generateBuildInfo") {
    packageName = "com.example.generated"
    version = project.version.toString()
    buildTime = java.time.LocalDateTime.now().toString()
    outputDir = layout.buildDirectory.dir("generated/sources/buildInfo/java")
}

// 把生成目录加到源码集
sourceSets["main"].java.srcDir(
    tasks.named<GenerateBuildInfoTask>("generateBuildInfo").map { it.outputDir }
)

// 让 compileJava 依赖此 task
tasks.named("compileJava") {
    dependsOn("generateBuildInfo")
}

./gradlew compileJava 时会自动先跑 generateBuildInfo,生成 BuildInfo.java,然后参与编译。典型代码生成场景

5.5.3 例 3:调外部命令的 Task

kotlin
abstract class GitInfoTask : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @get:Inject
    abstract val execOperations: ExecOperations

    @TaskAction
    fun run() {
        val out = java.io.ByteArrayOutputStream()
        execOperations.exec {
            commandLine("git", "rev-parse", "--short", "HEAD")
            standardOutput = out
        }
        val sha = out.toString().trim()
        outputFile.get().asFile.writeText("git.sha=$sha\n")
        println("🌿 Git SHA: $sha")
    }
}

tasks.register<GitInfoTask>("captureGitInfo") {
    outputFile = layout.buildDirectory.file("git.properties")
}

5.5.4 例 4:增量处理大量文件

kotlin
abstract class TransformFilesTask : DefaultTask() {
    @get:InputDirectory
    @get:Incremental
    abstract val sourceDir: DirectoryProperty

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @TaskAction
    fun execute(inputChanges: InputChanges) {
        if (!inputChanges.isIncremental) {
            // 全量执行(首次或缓存失效)
            outputDir.get().asFile.deleteRecursively()
            outputDir.get().asFile.mkdirs()
        }

        inputChanges.getFileChanges(sourceDir).forEach { change ->
            val target = outputDir.get().file(change.normalizedPath).asFile
            when (change.changeType) {
                ChangeType.ADDED, ChangeType.MODIFIED -> {
                    target.parentFile.mkdirs()
                    target.writeText(change.file.readText().uppercase())
                    println("📝 Transformed: ${change.normalizedPath}")
                }
                ChangeType.REMOVED -> {
                    target.delete()
                    println("❌ Deleted: ${change.normalizedPath}")
                }
            }
        }
    }
}

@Incremental + InputChanges 让 Task 只处理"变化的那部分文件",对处理千百个文件的 Task(如代码生成、资源处理)性能提升巨大。


5.6 Task 跨阶段交互:Provider / Property API

5.6.1 痛点:配置阶段拿不到执行阶段的值

kotlin
// ❌ 错的:configure 阶段 myTask 还没跑,outputFile 不存在
val taskA = tasks.register("myTask") {
    outputs.file("foo.txt")
    doLast { file("foo.txt").writeText("hello") }
}

val taskB = tasks.register("dependent") {
    val content = file("foo.txt").readText()  // ← 配置阶段就读,foo.txt 还没生成!
    doLast { println(content) }
}

5.6.2 解法:用 Provider 延迟取值

kotlin
// ✅ 正确:把"取值"包装成 Provider,执行时才求值
val taskA = tasks.register("myTask") {
    val output = layout.buildDirectory.file("foo.txt")
    outputs.file(output)
    doLast { output.get().asFile.writeText("hello") }
}

val taskB = tasks.register("dependent") {
    dependsOn(taskA)
    doLast {
        val content = taskA.get().outputs.files.singleFile.readText()
        println(content)
    }
}

5.6.3 优雅写法:用 Provider chaining

kotlin
val versionFile = layout.buildDirectory.file("version.txt")

val createVersion = tasks.register("createVersion") {
    outputs.file(versionFile)
    doLast {
        versionFile.get().asFile.writeText("1.2.3")
    }
}

val packageApp = tasks.register("packageApp") {
    inputs.file(versionFile)
    dependsOn(createVersion)
    doLast {
        val v = versionFile.get().asFile.readText().trim()
        println("📦 Packaging app version $v")
    }
}

📌 这里 versionFileProvider<RegularFile>,是懒求值的:直到 doLast.get() 时才真正读文件路径。这种模式 Configuration Cache 友好。


5.7 章末小结

                    ★ 第 5 章核心知识图谱 ★

        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
   ┌────▼────┐          ┌────▼────┐          ┌────▼─────┐
   │ 三种写法 │          │ 增量构建 │          │ 进阶 API │
   ├─────────┤          ├──────────┤          ├──────────┤
   │ doLast  │          │ inputs   │          │ Provider │
   │ Typed   │          │ outputs  │          │ Property │
   │ 自定义类 │          │ @Input   │          │ Optional │
   │         │          │ @Output  │          │ onlyIf   │
   └─────────┘          └──────────┘          └──────────┘

                                       ★ 性能差距 5-10 倍 ★

🎤 5.8 章末面试题(10 道高频题)

Q1. 同样是注册 Task,为什么用 tasks.registertasks.create 好?

register懒注册:只把"配置闭包"存起来,直到 Task 真的被需要时才执行配置。create立即创建:配置阶段一遇到就跑闭包。

好处

  1. 没用到的 Task 不耗时;
  2. 配置缓存友好;
  3. Gradle 5+ 推荐做法。

工程上默认全部用 register,不要用 create


Q2. 如何让一个自定义 Task 支持增量构建?

:3 步:

  1. 声明输入:用 @Input / @InputFile / @InputDirectory / @InputFiles 注解或 inputs.file(...) API。
  2. 声明输出:用 @OutputFile / @OutputDirectory 注解或 outputs.file(...) API。
  3. 真正的动作放在 @TaskAction 标记的方法里(自定义类)或 doLast {} 里(临时 task)。

声明完整了,Gradle 会自动比对 input/output 的 hash 决定是否跳过。


Q3. doFirst / doLast@TaskAction 的关系?

  • @TaskAction:自定义 Task 类主要执行体的标识,方法被调用时执行 Task 的核心逻辑。
  • doFirst { }:在 @TaskAction 之前插入额外动作(多个 doFirst 倒序执行)。
  • doLast { }:在 @TaskAction 之后追加额外动作。

执行顺序:doFirst-N → ... → doFirst-1 → @TaskAction → doLast-1 → ... → doLast-N

💡 99% 时候用 doLast 就够了,@TaskAction 是更"工程化"的写法。


Q4. @Input@InputFile 有什么区别?

  • @Input:标量输入(字符串、数字、布尔、枚举),值本身参与 hash 计算。
  • @InputFile:文件输入,文件内容的 hash 参与计算(不是路径!改名不算变)。

类似的:

  • @OutputFile 是文件,路径要明确;
  • @OutputDirectory 是目录,目录下所有文件都视为输出。

  • 如果一个属性只是路径不是要监听的文件内容,应该用 @PathSensitive(PathSensitivity.NONE) + @InputFile,否则换路径就触发重跑。

Q5. 我有一个 Task 不希望它每次都跑,怎么控制?

:3 种手段:

  1. 声明 input/output最佳)→ 没变就 UP-TO-DATE 自动跳过;
  2. onlyIf {} → 加条件判断,不满足就 SKIPPED;
  3. enabled = false → 临时禁用,所有时候都不跑。
手段何时用
input/output通用,最理想
onlyIf依赖动态条件(系统变量、project property)
enabled临时调试 / 永久禁用

Q6. 自定义 Task 类为啥要用 abstract class 而不是 open class

:Gradle 会自动生成抽象属性的实现(运行时通过字节码增强)。所以你只需要声明:

kotlin
abstract class MyTask : DefaultTask() {
    @get:Input
    abstract val input: Property<String>      // 抽象,Gradle 帮你实现

    @TaskAction
    fun run() {
        println(input.get())
    }
}

Gradle 会自动给 input 注入一个 Property<String> 实例。不写 abstract 反而要自己手动实现 Property,更麻烦


Q7. 一个 Task 怎么消费另一个 Task 的输出?

:用 Provider(懒求值)传递:

kotlin
val genTask = tasks.register<GenerateTask>("gen") {
    output = layout.buildDirectory.file("gen.txt")
}

val useTask = tasks.register("use") {
    inputs.file(genTask.flatMap { it.output })   // ← 用 flatMap 链 Provider
    doLast { /* ... */ }
}

好处

  1. 自动声明 task 依赖(Gradle 看到 inputs 来自 genTask 的 output,自动加 dependsOn);
  2. 懒求值,配置阶段不实际计算;
  3. 配置缓存友好。

反模式:直接 genTask.get().output.get() 在配置阶段取值 —— 会 eager 求值,破坏 lazy 优势。


Q8. @CacheableTask 注解什么时候加?

@CacheableTask 标记一个 Task 的输出可以被构建缓存(Build Cache)复用

  • 不加:只能"本地增量"(input/output 没变就跳过)
  • 加了:input 一致时能从远程 / 本地缓存取出 output,不用重跑

适合加的场景

  • 输入完全决定输出(纯函数式)
  • 输出是文件/目录(不是控制台输出之类的副作用)
  • 输入声明完整(包括 environment、JVM 版本等隐含因素)

典型例子compileJavaTestJar 这些内置 Task 都是 @CacheableTask


Q9. 怎么写一个"增量处理输入文件"的 Task?

:用 @Incremental + InputChanges

kotlin
abstract class TransformFilesTask : DefaultTask() {
    @get:InputDirectory
    @get:Incremental
    abstract val src: DirectoryProperty

    @get:OutputDirectory
    abstract val dst: DirectoryProperty

    @TaskAction
    fun run(changes: InputChanges) {
        if (!changes.isIncremental) {
            dst.get().asFile.deleteRecursively()
            dst.get().asFile.mkdirs()
        }
        changes.getFileChanges(src).forEach { c ->
            // 只处理变化的文件
            when (c.changeType) {
                ChangeType.ADDED, ChangeType.MODIFIED -> { /* ... */ }
                ChangeType.REMOVED -> { /* 删 */ }
            }
        }
    }
}

好处:源代码 1000 个文件,改 1 个 → 只处理 1 个,性能提升 1000 倍。常见于代码生成、资源处理、文档转换等场景。


Q10. tasks.namedtasks.findByName 有什么区别?

  • tasks.named("foo")懒求值,返回 TaskProvider<Task>。如果 task 不存在会抛 UnknownTaskException,但不会立即配置该 task。
  • tasks.findByName("foo")立即查找,返回 Task?(可能为 null)。会立即触发该 task 的配置

推荐:99% 情况下用 tasks.named,因为:

  1. 懒,更快;
  2. 配置缓存友好;
  3. 失败时报错更清晰。

findByName 只在"判断 task 是否存在 + 不需要配置它"的场景用。


下一章 → 第 6 章 · 依赖管理 →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

txt
/*
 * 第 5 章 · Task 全攻略 — 完整可跑示例
 *
 * 包含:
 *   - 三种 Task 写法(doLast / Typed / 自定义类)
 *   - 增量构建(input/output 声明)
 *   - 增量处理输入文件(@Incremental + InputChanges)
 *   - Provider 链式
 *   - onlyIf / enabled 控制执行
 *
 * 跑:
 *   ./gradlew tasks --group ch05-demo
 *   ./gradlew renderTemplates       —— 看增量构建效果(再跑一次会 UP-TO-DATE)
 *   ./gradlew countLines             —— 自定义 Task 类示例
 *   ./gradlew generateBuildInfo      —— 生成 BuildInfo.java
 *   ./gradlew releaseZip             —— 综合:依赖链 + Typed Task
 */

import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.gradle.work.ChangeType

plugins {
    base
}

version = "1.0.0"

// ===========================================================================
// 写法 1:临时 Task(doLast)
// 没声明 input/output → 每次都跑(演示反面教材)
// ===========================================================================
tasks.register("badTask") {
    group = "ch05-demo"
    description = "反面教材:每次都全量执行的 Task"
    doLast {
        println("⏰ Bad task: 我每次都被执行(即使没改任何东西)")
        Thread.sleep(800)
    }
}

// ===========================================================================
// 写法 1+:临时 Task + input/output 声明 → 增量构建生效
// ===========================================================================
tasks.register("renderTemplates") {
    group = "ch05-demo"
    description = "增量构建演示:渲染 templates/ 下的所有文件"

    val srcDir = layout.projectDirectory.dir("templates")
    val outDir = layout.buildDirectory.dir("rendered")

    inputs.dir(srcDir).optional()
    inputs.property("name", "World")
    outputs.dir(outDir)

    doLast {
        if (!srcDir.asFile.exists()) {
            srcDir.asFile.mkdirs()
            srcDir.asFile.resolve("hello.txt").writeText("Hello, {{name}}!")
        }
        outDir.get().asFile.mkdirs()
        srcDir.asFile.listFiles().orEmpty().forEach { f ->
            val rendered = f.readText().replace("{{name}}", "World")
            outDir.get().file(f.name).asFile.writeText(rendered)
        }
        println("✅ Rendered " + srcDir.asFile.listFiles().orEmpty().size + " templates")
    }
}

// ===========================================================================
// 写法 2:Typed Task —— 用内置类型
// ===========================================================================
tasks.register<Copy>("copyDocs") {
    group = "ch05-demo"
    description = "演示 Copy 类型"

    from("docs") {
        include("**/*.md")
    }
    into(layout.buildDirectory.dir("docs"))
}

tasks.register<Zip>("releaseZip") {
    group = "ch05-demo"
    description = "综合演示:聚合多个产物为 zip"

    dependsOn("copyDocs", "renderTemplates")
    archiveFileName = "release-" + project.version + ".zip"
    destinationDirectory = layout.buildDirectory.dir("releases")

    from(layout.buildDirectory.dir("docs")) { into("docs") }
    from(layout.buildDirectory.dir("rendered")) { into("rendered") }
}

// ===========================================================================
// 写法 3:自定义 Task 类(推荐)
// ===========================================================================
@CacheableTask
abstract class CountLinesTask : DefaultTask() {

    @get:InputDirectory
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val sourceDir: DirectoryProperty

    @get:Input
    abstract val fileExtension: Property<String>

    @get:OutputFile
    abstract val report: RegularFileProperty

    @TaskAction
    fun count() {
        val pattern = "**/*." + fileExtension.get()
        val files = sourceDir.get().asFileTree.matching { include(pattern) }.files
        val total = files.sumOf { it.readLines().size }

        report.get().asFile.parentFile.mkdirs()
        report.get().asFile.writeText("Files: " + files.size + "\nTotal lines: " + total + "\n")
        println("📊 Counted " + total + " lines in " + files.size + " files → " + report.get().asFile)
    }
}

tasks.register<CountLinesTask>("countLines") {
    group = "ch05-demo"
    description = "统计 Kotlin 源码行数"
    sourceDir = layout.projectDirectory
    fileExtension = "kts"
    report = layout.buildDirectory.file("reports/lines.txt")
}

// ===========================================================================
// 自定义 Task:生成 BuildInfo.java(典型代码生成场景)
// ===========================================================================
abstract class GenerateBuildInfoTask : DefaultTask() {
    @get:Input
    abstract val packageName: Property<String>

    @get:Input
    abstract val version: Property<String>

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @TaskAction
    fun generate() {
        val pkg = packageName.get()
        val pkgPath = pkg.replace('.', '/')
        val target = outputDir.get().asFile.resolve(pkgPath + "/BuildInfo.java")
        target.parentFile.mkdirs()

        target.writeText("""
            package $pkg;
            public final class BuildInfo {
                public static final String VERSION = "${'$'}{version.get()}";
                public static final String BUILD_TIME = "${'$'}{System.currentTimeMillis()}";
                private BuildInfo() {}
            }
        """.trimIndent())

        println("📝 Generated: " + target.absolutePath)
    }
}

tasks.register<GenerateBuildInfoTask>("generateBuildInfo") {
    group = "ch05-demo"
    description = "生成 BuildInfo.java 类(演示代码生成)"
    packageName = "com.example.generated"
    version = project.version.toString()
    outputDir = layout.buildDirectory.dir("generated/sources/buildInfo/java")
}

// ===========================================================================
// 增量处理输入文件:@Incremental + InputChanges
// ===========================================================================
abstract class TransformFilesTask : DefaultTask() {
    @get:InputDirectory
    @get:Incremental
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val src: DirectoryProperty

    @get:OutputDirectory
    abstract val dst: DirectoryProperty

    @TaskAction
    fun execute(changes: InputChanges) {
        if (!changes.isIncremental) {
            println("🔄 Full rebuild")
            dst.get().asFile.deleteRecursively()
            dst.get().asFile.mkdirs()
        } else {
            println("⚡ Incremental rebuild")
        }

        changes.getFileChanges(src).forEach { c ->
            val target = dst.get().file(c.normalizedPath).asFile
            when (c.changeType) {
                ChangeType.ADDED, ChangeType.MODIFIED -> {
                    target.parentFile.mkdirs()
                    target.writeText(c.file.readText().uppercase())
                    println("  📝 " + c.changeType + ": " + c.normalizedPath)
                }
                ChangeType.REMOVED -> {
                    target.delete()
                    println("  ❌ REMOVED: " + c.normalizedPath)
                }
            }
        }
    }
}

tasks.register<TransformFilesTask>("transformFiles") {
    group = "ch05-demo"
    description = "增量处理:只对变化的文件做转换"
    src = layout.projectDirectory.dir("data")
    dst = layout.buildDirectory.dir("transformed")
}

// ===========================================================================
// 演示 onlyIf
// ===========================================================================
tasks.register("uploadToS3") {
    group = "ch05-demo"
    description = "只在 -Prelease 时执行"
    onlyIf { project.hasProperty("release") }
    doLast { println("📤 Uploading to S3...") }
}

// ===========================================================================
// 演示 finalizedBy
// ===========================================================================
tasks.register("riskyOperation") {
    group = "ch05-demo"
    description = "演示 finalizedBy:A 跑完后无论成败都跑 cleanup"
    finalizedBy("cleanup")
    doLast { println("⚠️  Risky operation running...") }
}
tasks.register("cleanup") {
    group = "ch05-demo"
    doLast { println("🧹 Cleanup running (always after riskyOperation)") }
}
txt
rootProject.name = "ch05-task-demo"

build.gradle.kts ↗ · settings.gradle.kts ↗