引入增量构建支持

目录

  • 声明任务输入和输出
  • 推断任务依赖项
  • 通过自定义任务简化
  • 关于增量构建的最后说明
  • 任务输入、输出和依赖项 #

    JavaCompile这样的内置任务会声明一组输入(Java源文件)和一组输出(类文件)。Gradle利用这些信息来确定任务是否是最新的,是否需要执行任何工作。如果输入或输出没有任何更改,Gradle可以跳过该任务。总而言之,我们将这种行为称为Gradle的增量构建支持

    要利用增量构建支持,您需要向Gradle提供有关任务输入和输出的信息。有可能配置一个任务只具有输出。在执行任务之前,Gradle会检查输出,如果输出没有改变,就会跳过任务的执行。在实际构建中,任务通常也具有输入—包括源文件、资源和属性。在执行任务之前,Gradle会检查输入和输出都没有改变

    通常,一个任务的输出将作为另一个任务的输入。使这些任务之间的顺序正确非常重要,否则任务将以错误的顺序运行或根本不运行。Gradle不依赖于任务在构建脚本中定义的顺序。新任务是无序的,因此执行顺序可能在每次构建之间发生变化。您可以通过声明一个任务与另一个任务之间的依赖关系来明确告诉Gradle它们之间的顺序,例如consumer.dependsOn producer

    声明显式任务依赖项 #

    让我们看一个包含常见模式的示例项目。对于这个项目,我们需要创建一个zip文件,其中包含generator任务的输出。generator任务创建文件的实际方式并不重要—它会生成包含递增数字的文件。

    build.gradle #

    apply plugin: 'base'
    
    task generator() {
        doLast {
            def generatedFileDir = file("$buildDir/generated")
            generatedFileDir.mkdirs()
            for (int i=0; i<10; i++) {
                new File(generatedFileDir, "${i}.txt").text = i
            }
        }
    }
    
    task zip(type: Zip) {
        dependsOn generator
        from "$buildDir/generated"
    }
    

    构建可以工作,但构建脚本存在一些问题。generator任务的输出目录在zip任务中重复,并且zip任务的依赖项通过dependsOn显式设置。Gradle似乎每次都执行generator任务,但不执行zip任务。这是一个指出Gradle的最新检查与其他工具(如Make)不同的好时机。Gradle比较输入和输出的校验和,而不是仅比较文件的修改时间。即使generator任务每次都运行并覆盖其所有输出文件,内容也不会改变,因此zip任务不需要再次运行。zip任务输入的校验和没有改变。跳过最新任务可以使Gradle避免不必要的工作,并加快开发反馈循环。

    声明任务输入和输出 #

    现在,让我们来了解为什么generator任务似乎每次都会运行。如果我们通过运行带有--info的构建来查看Gradle的信息级别日志输出,我们将看到原因。

    Executing task ':generator' (up-to-date check took 0.0 secs) due to:
    Task has not declared any outputs.
    

    我们可以看到Gradle不知道该任务会产生任何输出。默认情况下,如果一个任务没有任何输出,它必须被视为非最新。输出使用TaskOutputs声明。任务输出可以是文件或目录。请注意下面的outputs用法。

    build.gradle #

    task generator() {
        def generatedFileDir = file("$buildDir/generated")
        outputs.dir generatedFileDir
        doLast {
            generatedFileDir.mkdirs()
            for (int i=0; i<10; i++) {
                new File(generatedFileDir, "${i}.txt").text = i
            }
        }
    }
    

    如果我们再运行两次构建,我们将看到第一次运行后generator任务显示它是最新的。如果我们再次查看--info输出,我们可以确认这一点。

    Skipping task ':generator' as it is up-to-date (took 0.007 secs).
    

    但是我们引入了一个新问题。如果我们增加生成的文件的数量(例如,从10增加到20),generator任务就不会重新运行。每次我们需要更改该参数时,我们可以通过执行清理构建来解决这个问题,但这种解决方法容易出错。

    我们可以告诉Gradle哪些因素会影响generator任务,并要求它重新执行。我们可以使用TaskInputs来声明某些属性作为任务的输入以及输入文件。如果这些输入中的任何一个发生更改,Gradle就会知道要执行该任务。请注意下面的inputs用法。

    build.gradle #

    task generator() {
        def fileCount = 10
        inputs.property "fileCount", fileCount
        def generatedFileDir = file("$buildDir/generated")
        outputs.dir generatedFileDir
        doLast {
            generatedFileDir.mkdirs()
            for (int i=0; i<fileCount; i++) {
                new File(generatedFileDir, "${i}.txt").text = i
            }
        }
    }
    

    我们可以通过在更改fileCount属性值后检查--info输出来验证这一点。

    Executing task ':generator' (up-to-date check took 0.007 secs) due to:
    Value of input property 'fileCount' has changed for task ':generator'
    

    推断任务依赖项 #

    到目前为止,我们只处理了generator任务,但我们还没有减少构建脚本中的任何重复项。我们有一个显式的任务依赖项和一个重复的输出目录路径。让我们尝试通过依赖CopySpec#from如何评估参数和Project#files来移除任务依赖项。Gradle可以自动为我们添加任务依赖项。这还将generator任务的输出添加为zip任务的输入。

    build.gradle #

    task zip(type: Zip) {
        from generator
    }
    

    当任务之间存在强大的生产者-消费者关系时,推断的任务依赖项比显式任务依赖项更容易维护。当您只需要另一个任务的部分输出时,显式任务依赖项通常会更清晰。如果您希望更容易理解,使用显式任务依赖项和推断依赖项并没有什么问题。

    通过自定义任务简化 #

    我们将像generator这样的任务称为临时任务。它们没有明确定义的属性,也没有预定义的动作来执行。使用临时任务来执行简单操作是可以的,但更好的做法是将临时任务移到自定义任务类中。自定义任务可以消除大量样板代码,并标准化构建中的常见操作。

    Gradle可以非常轻松地添加新的任务类型。您可以直接在构建文件中开始试验自定义任务类型。在使用@OutputDirectory等注解时,Gradle会在任务执行之前创建输出目录,因此您不必担心自己创建目录。其他注解,如@Input@InputFiles,与手动配置任务的TaskInputs效果相同。

    尝试创建一个名为Generate的自定义任务类,该类产生与上面generator任务相同的输出。您的构建文件应如下所示:

    build.gradle #

    task generator(type: Generate) {
        fileCount = 20
    }
    
    task zip(type: Zip) {
        from generator
    }
    

    这是我们的解决方案:

    build.gradle #

    class Generate extends DefaultTask {
        @Input
        int fileCount = 10
    
        @OutputDirectory
        File generatedFileDir = project.file("${project.buildDir}/generated")
    
        @TaskAction
        void perform() {
            for (int i=0; i<fileCount; i++) {
                new File(generatedFileDir, "${i}.txt").text = i
            }
        }
    }
    

    请注意,我们不再需要手动创建输出目录。generatedFileDir上的注解会为我们处理这个问题。fileCount上的注解告诉Gradle,此属性应被视为输入,就像我们之前使用inputs.property一样。最后,perform()上的注解定义了Generate任务的动作。

    关于增量构建的最后说明 #

    在开发自己的构建脚本、插件和自定义任务时,声明任务输入和输出是保留在工具箱中的一项重要技术。所有核心Gradle任务都有效地使用了这一点。如果您想了解在更低级别上使任务增量化的其他方法,请查看正在孵化中的增量任务支持。增量任务提供了一种细粒度的方式,在任务需要执行时仅构建已更改的部分。

    讨论