几乎每个 Gradle 构建都以某种方式与文件交互:想想源文件、文件依赖项、报告等。这就是 Gradle 提供全面 API 的原因,可以让您轻松执行所需的文件操作。

API 有两个部分:

  • 指定要处理的文件和目录

  • 指定如何处理它们

深度文件路径部分详细介绍了其中的第一部分,而后续部分(例如深度文件复制)则介绍了第二部分。首先,我们将向您展示用户遇到的最常见场景的示例。

复制单个文件

您可以通过创建 Gradle 内置复制任务的实例并使用文件的位置和要放置文件的位置来配置它来复制文件。此示例模拟将生成的报告复制到将打包到存档中的目录,例如 ZIP 或 TAR:

build.gradle.kts
tasks.register<Copy>("copyReport") {
    from(layout.buildDirectory.file("reports/my-report.pdf"))
    into(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('copyReport', Copy) {
    from layout.buildDirectory.file("reports/my-report.pdf")
    into layout.buildDirectory.dir("toArchive")
}

ProjectLayout用​​于查找相对于当前项目的文件或目录路径。这是使构建脚本无论项目路径如何都能正常工作的常用方法。然后,文件和目录路径用于指定使用Copy.from(java.lang.Object…​)复制哪个文件以及使用Copy.into(java.lang.Object)将其复制到哪个目录。

尽管硬编码路径可以提供简单的示例,但它们也会使构建变得脆弱。最好使用可靠的单一事实来源,例如任务或共享项目属性。在下面的修改示例中,我们使用在其他地方定义的报告任务,该任务将报告的位置存储在其outputFile属性中:

build.gradle.kts
tasks.register<Copy>("copyReport2") {
    from(myReportTask.flatMap { it.outputFile })
    into(archiveReportsTask.flatMap { it.dirToArchive })
}
build.gradle
tasks.register('copyReport2', Copy) {
    from myReportTask.outputFile
    into archiveReportsTask.dirToArchive
}

我们还假设报告将由 存档archiveReportsTask,这为我们提供了将存档的目录,以及我们想要放置报告副本的位置。

复制多个文件

您可以通过提供多个参数来轻松地将前面的示例扩展到多个文件from()

build.gradle.kts
tasks.register<Copy>("copyReportsForArchiving") {
    from(layout.buildDirectory.file("reports/my-report.pdf"), layout.projectDirectory.file("src/docs/manual.pdf"))
    into(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('copyReportsForArchiving', Copy) {
    from layout.buildDirectory.file("reports/my-report.pdf"), layout.projectDirectory.file("src/docs/manual.pdf")
    into layout.buildDirectory.dir("toArchive")
}

现在有两个文件被复制到存档目录中。您还可以使用多个语句来执行相同的操作,如深度文件复制from()部分的第一个示例所示。

现在考虑另一个例子:如果您想复制目录中的所有 PDF 而不必指定每个 PDF,该怎么办?为此,请将包含和/或排除模式附加到复制规范。这里我们使用字符串模式仅包含 PDF:

build.gradle.kts
tasks.register<Copy>("copyPdfReportsForArchiving") {
    from(layout.buildDirectory.dir("reports"))
    include("*.pdf")
    into(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('copyPdfReportsForArchiving', Copy) {
    from layout.buildDirectory.dir("reports")
    include "*.pdf"
    into layout.buildDirectory.dir("toArchive")
}

需要注意的一件事是,如下图所示,仅reports复制直接驻留在该目录中的 PDF:

使用平面过滤器复制示例
图 1. 平面过滤器对复印的影响

您可以使用 Ant 风格的 glob 模式 ( **/*) 将文件包含在子目录中,如本更新示例中所示:

build.gradle.kts
tasks.register<Copy>("copyAllPdfReportsForArchiving") {
    from(layout.buildDirectory.dir("reports"))
    include("**/*.pdf")
    into(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('copyAllPdfReportsForArchiving', Copy) {
    from layout.buildDirectory.dir("reports")
    include "**/*.pdf"
    into layout.buildDirectory.dir("toArchive")
}

该任务有以下效果:

使用深层过滤器复制示例
图 2. 深度过滤器对复制的影响

reports需要记住的一件事是,像这样的深层过滤器会产生复制下面的目录结构以及文件的副作用。如果只想复制没有目录结构的文件,则需要使用显式表达式。我们在文件树部分详细讨论文件树和文件集合之间的区别。fileTree(dir) { includes }.files

这只是在 Gradle 构建中处理文件操作时可能遇到的行为变化之一。幸运的是,Gradle 为几乎所有这些用例提供了优雅的解决方案。请阅读本章后面的深入部分,了解有关 Gradle 中文件操作如何工作以及配置它们的选项的更多详细信息。

复制目录层次结构

您可能不仅需要复制文件,还需要复制它们所在的目录结构。这是指定目录作为参数时的默认行为from(),如以下示例所示,该示例将reports目录中的所有内容(包括其所有子目录)复制到目标:

build.gradle.kts
tasks.register<Copy>("copyReportsDirForArchiving") {
    from(layout.buildDirectory.dir("reports"))
    into(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('copyReportsDirForArchiving', Copy) {
    from layout.buildDirectory.dir("reports")
    into layout.buildDirectory.dir("toArchive")
}

用户面临的关键问题是控制有多少目录结构到达目的地。在上面的示例中,您是否获得一个toArchive/reports目录,或者其中的所有内容都reports直接进入toArchive?答案是后者。如果目录是from()路径的一部分,那么它不会出现在目标中。

那么如何确保reports自身被复制,而不是 中的任何其他目录${layout.buildDirectory}?答案是将其添加为包含模式:

build.gradle.kts
tasks.register<Copy>("copyReportsDirForArchiving2") {
    from(layout.buildDirectory) {
        include("reports/**")
    }
    into(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('copyReportsDirForArchiving2', Copy) {
    from(layout.buildDirectory) {
        include "reports/**"
    }
    into layout.buildDirectory.dir("toArchive")
}

您将得到与以前相同的行为,除了在目标中多了一层目录,即toArchive/reports.

需要注意的一件事是该include()指令仅适用于from(),而上一节中的指令适用于整个任务。复制规范中的这些不同粒度级别使您能够轻松处理您将遇到的大多数需求。您可以在子规范部分了解更多相关信息。

创建档案(zip、tar 等)

从 Gradle 的角度来看,将文件打包到存档中实际上是一个副本,其中目标是存档文件而不是文件系统上的目录。这意味着创建档案看起来很像复制,并且具有所有相同的功能!

最简单的情况涉及归档目录的全部内容,本示例通过创建目录的 ZIP 来演示toArchive

build.gradle.kts
tasks.register<Zip>("packageDistribution") {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = layout.buildDirectory.dir("dist")

    from(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('packageDistribution', Zip) {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = layout.buildDirectory.dir('dist')

    from layout.buildDirectory.dir("toArchive")
}

请注意我们如何指定存档的目标和名称而不是into():两者都是必需的。您通常不会看到它们被显式设置,因为大多数项目都应用Base Plugin。它为这些属性提供了一些常规值。下一个示例演示了这一点,您可以了解有关存档命名部分中的约定的更多信息。

每种类型的存档都有自己的任务类型,最常见的是ZipTarJar。它们都共享 的大部分配置选项Copy,包括过滤和重命名。

最常见的场景之一是将文件复制到存档的指定子目录中。例如,假设您要将所有 PDF 打包到docs存档根目录中的一个目录中。源位置中不存在此docs目录,因此您必须将其创建为存档的一部分。您可以通过into()仅添加 PDF 声明来完成此操作:

build.gradle.kts
plugins {
    base
}

version = "1.0.0"

tasks.register<Zip>("packageDistribution") {
    from(layout.buildDirectory.dir("toArchive")) {
        exclude("**/*.pdf")
    }

    from(layout.buildDirectory.dir("toArchive")) {
        include("**/*.pdf")
        into("docs")
    }
}
build.gradle
plugins {
    id 'base'
}

version = "1.0.0"

tasks.register('packageDistribution', Zip) {
    from(layout.buildDirectory.dir("toArchive")) {
        exclude "**/*.pdf"
    }

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

正如您所看到的,您可以from()在副本规范中拥有多个声明,每个声明都有自己的配置。有关此功能的更多信息,请参阅使用子副本规范。

解压档案

存档实际上是独立的文件系统,因此解压它们就是将文件从该文件系统复制到本地文件系统,甚至复制到另一个存档中。 Gradle 通过提供一些包装函数来实现这一点,这些函数使档案可以作为文件的分层集合(文件树)使用。

感兴趣的两个函数是Project.zipTree(java.lang.Object)Project.tarTree(java.lang.Object) ,它们从相应的归档文件生成FileTree 。然后可以在规范中使用该文件树from(),如下所示:

build.gradle.kts
tasks.register<Copy>("unpackFiles") {
    from(zipTree("src/resources/thirdPartyResources.zip"))
    into(layout.buildDirectory.dir("resources"))
}
build.gradle
tasks.register('unpackFiles', Copy) {
    from zipTree("src/resources/thirdPartyResources.zip")
    into layout.buildDirectory.dir("resources")
}

与普通副本一样,您可以通过过滤器控制解包哪些文件,甚至可以在解包时重命名文件。

更高级的处理可以通过eachFile()方法进行。例如,您可能需要将存档的不同子树提取到目标目录中的不同路径中。以下示例使用该方法将存档libs目录中的文件提取到根目标目录中,而不是提取到libs子目录中:

build.gradle.kts
tasks.register<Copy>("unpackLibsDirectory") {
    from(zipTree("src/resources/thirdPartyResources.zip")) {
        include("libs/**")  (1)
        eachFile {
            relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())  (2)
        }
        includeEmptyDirs = false  (3)
    }
    into(layout.buildDirectory.dir("resources"))
}
build.gradle
tasks.register('unpackLibsDirectory', Copy) {
    from(zipTree("src/resources/thirdPartyResources.zip")) {
        include "libs/**"  (1)
        eachFile { fcd ->
            fcd.relativePath = new RelativePath(true, fcd.relativePath.segments.drop(1))  (2)
        }
        includeEmptyDirs = false  (3)
    }
    into layout.buildDirectory.dir("resources")
}
1 libs仅提取驻留在目录中的文件子集
2 libs通过从文件路径中删除段,将提取文件的路径重新映射到目标目录
3 忽略重新映射产生的空目录,请参阅下面的注意事项

您无法使用此技术更改空目录的目标路径。您可以在本期了解更多信息

如果您是一名 Java 开发人员并且想知道为什么没有jarTree()方法,那是因为zipTree()它非常适合 JAR、WAR 和 EAR。

创建“uber”或“fat”JAR

在 Java 领域,应用程序及其依赖项通常打包为单个分发存档中的单独 JAR。这种情况仍然会发生,但现在还有另一种常见的方法:将依赖项的类和资源直接放入应用程序 JAR 中,创建所谓的 uber 或 fat JAR。

Gradle 使这种方法很容易实现。考虑目标:将其他 JAR 文件的内容复制到应用程序 JAR 中。为此,您需要的只是Project.zipTree(java.lang.Object)方法和JaruberJar任务,如以下示例中的任务所示:

build.gradle.kts
plugins {
    java
}

version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation("commons-io:commons-io:2.6")
}

tasks.register<Jar>("uberJar") {
    archiveClassifier = "uber"

    from(sourceSets.main.get().output)

    dependsOn(configurations.runtimeClasspath)
    from({
        configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
    })
}
build.gradle
plugins {
    id 'java'
}

version = '1.0.0'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'commons-io:commons-io:2.6'
}

tasks.register('uberJar', Jar) {
    archiveClassifier = 'uber'

    from sourceSets.main.output

    dependsOn configurations.runtimeClasspath
    from {
        configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
    }
}

在本例中,我们将获取项目的运行时依赖项 —configurations.runtimeClasspath.files并使用该方法包装每个 JAR 文件zipTree()。结果是 ZIP 文件树的集合,其内容与应用程序类一起复制到 uber JAR 中。

创建目录

许多任务需要创建目录来存储它们生成的文件,这就是为什么 Gradle 在显式定义文件和目录输出时自动管理任务的这方面。您可以在用户手册的增量构建部分了解此功能。所有核心 Gradle 任务都确保在必要时使用此机制创建它们所需的任何输出目录。

如果您需要手动创建目录,您可以使用构建脚本或自定义任务实现中的标准Files.createDirectories或方法。这是一个在项目文件夹中File.mkdirs创建单个目录的简单示例:images

build.gradle.kts
tasks.register("ensureDirectory") {
    // Store target directory into a variable to avoid project reference in the configuration cache
    val directory = file("images")

    doLast {
        Files.createDirectories(directory.toPath())
    }
}
build.gradle
tasks.register('ensureDirectory') {
    // Store target directory into a variable to avoid project reference in the configuration cache
    def directory = file("images")

    doLast {
        Files.createDirectories(directory.toPath())
    }
}

正如Apache Ant 手册中所述,该mkdir任务将自动在给定路径中创建所有必需的目录,如果该目录已存在,则不会执行任何操作。

移动文件和目录

Gradle 没有用于移动文件和目录的 API,但您可以使用Apache Ant 集成轻松做到这一点,如本示例所示:

build.gradle.kts
tasks.register("moveReports") {
    // Store the build directory into a variable to avoid project reference in the configuration cache
    val dir = buildDir

    doLast {
        ant.withGroovyBuilder {
            "move"("file" to "${dir}/reports", "todir" to "${dir}/toArchive")
        }
    }
}
build.gradle
tasks.register('moveReports') {
    // Store the build directory into a variable to avoid project reference in the configuration cache
    def dir = buildDir

    doLast {
        ant.move file: "${dir}/reports",
                 todir: "${dir}/toArchive"
    }
}

这不是一个常见的要求,应该谨慎使用,因为您会丢失信息并且很容易破坏构建。通常最好是复制目录和文件。

重命名副本上的文件

构建使用和生成的文件有时没有合适的名称,在这种情况下,您需要在复制这些文件时重命名它们。 Gradle 允许您使用配置将其作为复制规范的一部分来执行rename()

以下示例从包含“-staging”标记的任何文件的名称中删除该标记:

build.gradle.kts
tasks.register<Copy>("copyFromStaging") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWar"))

    rename("(.+)-staging(.+)", "$1$2")
}
build.gradle
tasks.register('copyFromStaging', Copy) {
    from "src/main/webapp"
    into layout.buildDirectory.dir('explodedWar')

    rename '(.+)-staging(.+)', '$1$2'
}

您可以使用正则表达式(如上面的示例所示),或者使用更复杂的逻辑来确定目标文件名的闭包。例如,以下任务截断文件名:

build.gradle.kts
tasks.register<Copy>("copyWithTruncate") {
    from(layout.buildDirectory.dir("reports"))
    rename { filename: String ->
        if (filename.length > 10) {
            filename.slice(0..7) + "~" + filename.length
        }
        else filename
    }
    into(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('copyWithTruncate', Copy) {
    from layout.buildDirectory.dir("reports")
    rename { String filename ->
        if (filename.size() > 10) {
            return filename[0..7] + "~" + filename.size()
        }
        else return filename
    }
    into layout.buildDirectory.dir("toArchive")
}

与过滤一样,您还可以通过将文件子集配置为from().

删除文件和目录

您可以使用删除任务或Project.delete(org.gradle.api.Action)方法轻松删除文件和目录。在这两种情况下,您都可以以Project.files(java.lang.Object…​)方法支持的方式指定要删除的文件和目录。

例如,以下任务删除构建输出目录的全部内容:

build.gradle.kts
tasks.register<Delete>("myClean") {
    delete(buildDir)
}
build.gradle
tasks.register('myClean', Delete) {
    delete buildDir
}

如果您想要更好地控制删除哪些文件,则不能像复制文件一样使用包含和排除。相反,您必须使用FileCollection和的内置过滤机制FileTree。以下示例就是从源目录中清除临时文件:

build.gradle.kts
tasks.register<Delete>("cleanTempFiles") {
    delete(fileTree("src").matching {
        include("**/*.tmp")
    })
}
build.gradle
tasks.register('cleanTempFiles', Delete) {
    delete fileTree("src").matching {
        include "**/*.tmp"
    }
}

您将在下一节中了解有关文件集合和文件树的更多信息。

深度文件路径

为了对文件执行某些操作,您需要知道它在哪里,这就是文件路径提供的信息。 Gradle 构建在标准 JavaFile类的基础上,该类表示单个文件的位置,并提供新的 API 来处理路径集合。本部分向您展示如何使用 Gradle API 指定用于任务和文件操作的文件路径。

但首先,关于在构建中使用硬编码文件路径的重要说明。

在硬编码文件路径上

本章中的许多示例使用硬编码路径作为字符串文字。这使得它们很容易理解,但这对于真正的构建来说并不是一个好的实践。问题是路径经常改变,并且需要改变路径的地方越多,就越有可能错过一个路径并破坏构建。

如果可能,您应该使用任务、任务属性和项目属性(按优先顺序)来配置文件路径。例如,如果您要创建一个打包 Java 应用程序的已编译类的任务,那么您的目标应该是这样的:

build.gradle.kts
val archivesDirPath = layout.buildDirectory.dir("archives")

tasks.register<Zip>("packageClasses") {
    archiveAppendix = "classes"
    destinationDirectory = archivesDirPath

    from(tasks.compileJava)
}
build.gradle
def archivesDirPath = layout.buildDirectory.dir('archives')

tasks.register('packageClasses', Zip) {
    archiveAppendix = "classes"
    destinationDirectory = archivesDirPath

    from compileJava
}

了解我们如何使用该compileJava任务作为要打包的文件的源,并且我们创建了一个项目属性archivesDirPath来存储放置档案的位置,基于我们可能在构建中的其他地方使用它。

像这样直接使用任务作为参数依赖于它已定义的输出,因此它并不总是可能的。此外,这个示例可以通过依赖 Java 插件的约定而destinationDirectory不是覆盖它来进一步改进,但它确实演示了项目属性的使用。

单个文件和目录

Gradle 提供了Project.file(java.lang.Object)方法来指定单个文件或目录的位置。相对路径是相对于项目目录解析的,而绝对路径保持不变。

new File(relative path)除非传递给file()orfiles()from()根据file()or定义的其他方法,否则切勿使用files()。否则,这将创建相对于当前工作目录 (CWD) 的路径。 Gradle 无法保证 CWD 的位置,这意味着依赖它的构建可能随时中断。

以下是使用file()具有不同类型参数的方法的一些示例:

Example 20. Locating files
build.gradle.kts
// Using a relative path
var configFile = file("src/config.xml")

// Using an absolute path
configFile = file(configFile.absolutePath)

// Using a File object with a relative path
configFile = file(File("src/config.xml"))

// Using a java.nio.file.Path object with a relative path
configFile = file(Paths.get("src", "config.xml"))

// Using an absolute java.nio.file.Path object
configFile = file(Paths.get(System.getProperty("user.home")).resolve("global-config.xml"))
build.gradle
// Using a relative path
File configFile = file('src/config.xml')

// Using an absolute path
configFile = file(configFile.absolutePath)

// Using a File object with a relative path
configFile = file(new File('src/config.xml'))

// Using a java.nio.file.Path object with a relative path
configFile = file(Paths.get('src', 'config.xml'))

// Using an absolute java.nio.file.Path object
configFile = file(Paths.get(System.getProperty('user.home')).resolve('global-config.xml'))

正如您所看到的,您可以将字符串、File实例和Path实例传递给file()方法,所有这些都会产生绝对File对象。您可以在上一段中链接的参考指南中找到参数类型的其他选项。

在多项目构建的情况下会发生什么?该file()方法始终将相对路径转换为相对于当前项目目录(可能是子项目)的路径。如果要使用相对于项目根目录的路径,则需要使用特殊的Project.getRootDir()属性来构造绝对路径,如下所示:

build.gradle.kts
val configFile = file("$rootDir/shared/config.xml")
build.gradle
File configFile = file("$rootDir/shared/config.xml")

假设您正在目录中进行多项目构建dev/projects/AcmeHealth。您可以在正在修复的库的构建中使用上面的示例 - at AcmeHealth/subprojects/AcmePatientRecordLib/build.gradle。文件路径将解析为dev/projects/AcmeHealth/shared/config.xml.

file()方法可用于配置具有 type 属性的任何任务File。不过,许多任务都处理多个文件,因此我们接下来看看如何指定文件集。

文件收藏

文件集合只是由FileCollection接口表示的一组文件路径。任何文件路径。重要的是要了解文件路径不必以任何方式相关,因此它们不必位于同一目录中,甚至不必具有共享的父目录。您还会发现 Gradle API 的许多部分都被使用FileCollection,例如本章后面讨论的复制 API 和依赖项配置

指定文件集合的推荐方法是使用ProjectLayout.files(java.lang.Object...)方法,该方法返回一个FileCollection实例。此方法非常灵活,允许您传递多个字符串、File实例、字符串集合、s 集合File等。如果任务已定义输出,您甚至可以将其作为参数传递。在参考指南中了解所有支持的参数类型。

files()正确处理相对路径和File(relative path)实例,相对于项目目录解析它们。

上一节中介绍的Project.file(java.lang.Object)方法一样,所有相对路径都是相对于当前项目目录进行评估的。以下示例演示了您可以使用的一些参数类型 - 字符串、实例、列表和 a :FilePath

build.gradle.kts
val collection: FileCollection = layout.files(
    "src/file1.txt",
    File("src/file2.txt"),
    listOf("src/file3.csv", "src/file4.csv"),
    Paths.get("src", "file5.txt")
)
build.gradle
FileCollection collection = layout.files('src/file1.txt',
                                  new File('src/file2.txt'),
                                  ['src/file3.csv', 'src/file4.csv'],
                                  Paths.get('src', 'file5.txt'))

文件集合在 Gradle 中具有一些重要的属性。他们可以:

  • 懒惰地创建

  • 迭代

  • 过滤的

  • 合并的

当您需要在构建运行时评估组成集合的文件时,延迟创建文件集合非常有用。在下面的示例中,我们查询文件系统以找出特定目录中存在哪些文件,然后将它们放入文件集合中:

build.gradle.kts
tasks.register("list") {
    val projectDirectory = layout.projectDirectory
    doLast {
        var srcDir: File? = null

        val collection = projectDirectory.files({
            srcDir?.listFiles()
        })

        srcDir = projectDirectory.file("src").asFile
        println("Contents of ${srcDir.name}")
        collection.map { it.relativeTo(projectDirectory.asFile) }.sorted().forEach { println(it) }

        srcDir = projectDirectory.file("src2").asFile
        println("Contents of ${srcDir.name}")
        collection.map { it.relativeTo(projectDirectory.asFile) }.sorted().forEach { println(it) }
    }
}
build.gradle
tasks.register('list') {
    Directory projectDirectory = layout.projectDirectory
    doLast {
        File srcDir

        // Create a file collection using a closure
        collection = projectDirectory.files { srcDir.listFiles() }

        srcDir = projectDirectory.file('src').asFile
        println "Contents of $srcDir.name"
        collection.collect { projectDirectory.asFile.relativePath(it) }.sort().each { println it }

        srcDir = projectDirectory.file('src2').asFile
        println "Contents of $srcDir.name"
        collection.collect { projectDirectory.asFile.relativePath(it) }.sort().each { println it }
    }
}
输出gradle -q list
> gradle -q list
Contents of src
src/dir1
src/file1.txt
Contents of src2
src2/dir1
src2/dir2

延迟创建的关键是将闭包(在 Groovy 中)或 a Provider(在 Kotlin 中)传递给files()方法。您的闭包/提供者只需要返回 接受的类型的值files(),例如List<File>StringFileCollection等。

可以通过集合上的each()方法(在 Groovy 中)或forEach方法(在 Kotlin 中)或在循环中使用集合来迭代文件集合for。在这两种方法中,文件集合都被视为一组File实例,即迭代变量的类型为File

以下示例演示了此类迭代以及如何使用as运算符或支持的属性将文件集合转换为其他类型:

build.gradle.kts
// Iterate over the files in the collection
collection.forEach { file: File ->
    println(file.name)
}

// Convert the collection to various types
val set: Set<File> = collection.files
val list: List<File> = collection.toList()
val path: String = collection.asPath
val file: File = collection.singleFile

// Add and subtract collections
val union = collection + projectLayout.files("src/file2.txt")
val difference = collection - projectLayout.files("src/file2.txt")
build.gradle
// Iterate over the files in the collection
collection.each { File file ->
    println file.name
}

// Convert the collection to various types
Set set = collection.files
Set set2 = collection as Set
List list = collection as List
String path = collection.asPath
File file = collection.singleFile

// Add and subtract collections
def union = collection + projectLayout.files('src/file2.txt')
def difference = collection - projectLayout.files('src/file2.txt')

您还可以在示例的末尾看到如何使用+-运算符来合并和减go文件集合。生成的文件集合的一个重要特征是它们是实时的。换句话说,当您以这种方式组合文件集合时,结果始终反映源文件集合中当前的内容,即使它们在构建过程中发生了变化。

例如,假设在上面的示例中创建collection后获得一两个额外的文件。union只要您union将这些文件添加到后使用collectionunion也会包含那些附加文件。文件收集也是如此different

过滤方面,实时收藏也很重要。如果要使用文件集合的子集,可以利用FileCollection.filter(org.gradle.api.specs.Spec)方法来确定要“保留”哪些文件。在以下示例中,我们创建一个新集合,其中仅包含源集合中以 .txt 结尾的文件:

build.gradle.kts
val textFiles: FileCollection = collection.filter { f: File ->
    f.name.endsWith(".txt")
}
build.gradle
FileCollection textFiles = collection.filter { File f ->
    f.name.endsWith(".txt")
}
输出gradle -q filterTextFiles
> gradle -q filterTextFiles
src/file1.txt
src/file2.txt
src/file5.txt

如果collection任何时候发生更改,无论是通过添加或删除文件本身,textFiles都会立即反映更改,因为它也是一个实时集合。请注意,您传递给的闭包filter()采用 aFile作为参数,并且应该返回一个布尔值。

文件树

文件是一个文件集合,保留其包含的文件的目录结构并具有FileTree类型。这意味着文件树中的所有路径都必须有一个共享的父目录。下图强调了在复制文件的常见情况下文件树和文件集合之间的区别:

文件集合与文件树
图 3. 复制文件时文件树和文件集合行为方式的差异
虽然FileTree扩展FileCollection(is-a 关系),但它们的行为确实不同。换句话说,只要需要文件集合,您就可以使用文件树,但请记住:文件集合是文件的平面列表/集,而文件树是文件和目录层次结构。要将文件树转换为平面集合,请使用FileTree.getFiles()属性。

创建文件树的最简单方法是将文件或目录路径传递给Project.fileTree(java.lang.Object)方法。这将创建该基目录中所有文件和目录的树(但不是基目录本身)。以下示例演示了如何使用基本方法,以及如何使用 Ant 样式模式过滤文件和目录:

build.gradle.kts
// Create a file tree with a base directory
var tree: ConfigurableFileTree = fileTree("src/main")

// Add include and exclude patterns to the tree
tree.include("**/*.java")
tree.exclude("**/Abstract*")

// Create a tree using closure
tree = fileTree("src") {
    include("**/*.java")
}

// Create a tree using a map
tree = fileTree("dir" to "src", "include" to "**/*.java")
tree = fileTree("dir" to "src", "includes" to listOf("**/*.java", "**/*.xml"))
tree = fileTree("dir" to "src", "include" to "**/*.java", "exclude" to "**/*test*/**")
build.gradle
// Create a file tree with a base directory
ConfigurableFileTree tree = fileTree(dir: 'src/main')

// Add include and exclude patterns to the tree
tree.include '**/*.java'
tree.exclude '**/Abstract*'

// Create a tree using closure
tree = fileTree('src') {
    include '**/*.java'
}

// Create a tree using a map
tree = fileTree(dir: 'src', include: '**/*.java')
tree = fileTree(dir: 'src', includes: ['**/*.java', '**/*.xml'])
tree = fileTree(dir: 'src', include: '**/*.java', exclude: '**/*test*/**')

您可以在PatternFilterable的 API 文档中查看支持模式的更多示例。另外,请参阅 API 文档,fileTree()了解可以作为基本目录传递的类型。

默认情况下,为了方便起见,fileTree()返回一个FileTree应用一些默认排除模式的实例——实际上与 Ant 的默认值相同。有关完整的默认排除列表,请参阅Ant 手册

如果这些默认排除被证明有问题,您可以通过更改设置脚本中的默认排除来解决该问题:

settings.gradle.kts
import org.apache.tools.ant.DirectoryScanner

DirectoryScanner.removeDefaultExclude("**/.git")
DirectoryScanner.removeDefaultExclude("**/.git/**")
settings.gradle
import org.apache.tools.ant.DirectoryScanner

DirectoryScanner.removeDefaultExclude('**/.git')
DirectoryScanner.removeDefaultExclude('**/.git/**')
目前,Gradle 的默认排除是通过 Ant 的DirectoryScanner类配置的。
Gradle 不支持在执行阶段更改默认排除。

您可以使用文件树执行许多与文件集合相同的操作:

您还可以使用FileTree.visit(org.gradle.api.Action)方法遍历文件树。以下示例演示了所有这些技术:

Example 28. Using a file tree
build.gradle.kts
// Iterate over the contents of a tree
tree.forEach{ file: File ->
    println(file)
}

// Filter a tree
val filtered: FileTree = tree.matching {
    include("org/gradle/api/**")
}

// Add trees together
val sum: FileTree = tree + fileTree("src/test")

// Visit the elements of the tree
tree.visit {
    println("${this.relativePath} => ${this.file}")
}
build.gradle
// Iterate over the contents of a tree
tree.each {File file ->
    println file
}

// Filter a tree
FileTree filtered = tree.matching {
    include 'org/gradle/api/**'
}

// Add trees together
FileTree sum = tree + fileTree(dir: 'src/test')

// Visit the elements of the tree
tree.visit {element ->
    println "$element.relativePath => $element.file"
}

我们已经讨论了如何创建自己的文件树和文件集合,但还值得记住的是,许多 Gradle 插件提供了自己的文件树实例,例如Java 的源集。它们的使用和操作方式与您自己创建的文件树完全相同。

用户通常需要的另一种特定类型的文件树是存档,即 ZIP 文件、TAR 文件等。接下来我们将讨论这些文件。

使用档案作为文件树

存档是打包到单个文件中的目录和文件层次结构。换句话说,它是文件树的特例,这正是 Gradle 处理档案的方式。您不使用fileTree()仅适用于普通文件系统的方法,而是使用Project.zipTree(java.lang.Object)Project.tarTree(java.lang.Object)方法来包装相应类型的存档文件(请注意JAR、WAR 和 EAR 文件是 ZIP)。这两种方法都会返回FileTree实例,然后您可以像普通文件树一样使用这些实例。例如,您可以通过将存档的内容复制到文件系统上的某个目录来提取存档的部分或全部文件。或者您可以将一个存档合并到另一个存档中。

以下是创建基于存档的文件树的一些简单示例:

build.gradle.kts
// Create a ZIP file tree using path
val zip: FileTree = zipTree("someFile.zip")

// Create a TAR file tree using path
val tar: FileTree = tarTree("someFile.tar")

// tar tree attempts to guess the compression based on the file extension
// however if you must specify the compression explicitly you can:
val someTar: FileTree = tarTree(resources.gzip("someTar.ext"))
build.gradle
// Create a ZIP file tree using path
FileTree zip = zipTree('someFile.zip')

// Create a TAR file tree using path
FileTree tar = tarTree('someFile.tar')

//tar tree attempts to guess the compression based on the file extension
//however if you must specify the compression explicitly you can:
FileTree someTar = tarTree(resources.gzip('someTar.ext'))

您可以在我们介绍的常见场景中看到提取存档文件的实际示例。

了解到文件集合的隐式转换

Gradle 中的许多对象都具有接受一组输入文件的属性。例如,JavaCompile任务有一个source定义要编译的源文件的属性。您可以使用files()方法支持的任何类型来设置此属性的值,如 API 文档中所述。这意味着您可以将属性设置为FileString、 集合,FileCollection甚至是闭包 或Provider

这是特定任务的一个特点!这意味着隐式转换不会发生在任何具有FileCollectionFileTree属性的任务上。如果你想知道在特定情况下是否发生隐式转换,你需要阅读相关文档,例如相应任务的API文档。或者,您可以通过在构建中显式使用ProjectLayout.files(java.lang.Object...) 来消除所有疑问。

以下是该属性可以采用的不同类型参数的一些示例source

build.gradle.kts
tasks.register<JavaCompile>("compile") {
    // Use a File object to specify the source directory
    source = fileTree(file("src/main/java"))

    // Use a String path to specify the source directory
    source = fileTree("src/main/java")

    // Use a collection to specify multiple source directories
    source = fileTree(listOf("src/main/java", "../shared/java"))

    // Use a FileCollection (or FileTree in this case) to specify the source files
    source = fileTree("src/main/java").matching { include("org/gradle/api/**") }

    // Using a closure to specify the source files.
    setSource({
        // Use the contents of each zip file in the src dir
        file("src").listFiles().filter { it.name.endsWith(".zip") }.map { zipTree(it) }
    })
}
build.gradle
tasks.register('compile', JavaCompile) {

    // Use a File object to specify the source directory
    source = file('src/main/java')

    // Use a String path to specify the source directory
    source = 'src/main/java'

    // Use a collection to specify multiple source directories
    source = ['src/main/java', '../shared/java']

    // Use a FileCollection (or FileTree in this case) to specify the source files
    source = fileTree(dir: 'src/main/java').matching { include 'org/gradle/api/**' }

    // Using a closure to specify the source files.
    source = {
        // Use the contents of each zip file in the src dir
        file('src').listFiles().findAll {it.name.endsWith('.zip')}.collect { zipTree(it) }
    }
}

另一件需要注意的事情是,诸如此类的属性source在核心 Gradle 任务中具有相应的方法。这些方法遵循附加到值集合而不是替换它们的约定。同样,此方法接受files()方法支持的任何类型,如下所示:

build.gradle.kts
tasks.named<JavaCompile>("compile") {
    // Add some source directories use String paths
    source("src/main/java", "src/main/groovy")

    // Add a source directory using a File object
    source(file("../shared/java"))

    // Add some source directories using a closure
    setSource({ file("src/test/").listFiles() })
}
build.gradle
compile {
    // Add some source directories use String paths
    source 'src/main/java', 'src/main/groovy'

    // Add a source directory using a File object
    source file('../shared/java')

    // Add some source directories using a closure
    source { file('src/test/').listFiles() }
}

由于这是常见约定,我们建议您在自己的自定义任务中遵循它。具体来说,如果您计划添加一个方法来配置基于集合的属性,请确保该方法附加而不是替换值。

文件深度复制

在 Gradle 中复制文件的基本过程很简单:

  • 定义复制类型的任务

  • 指定要复制的文件(以及可能的目录)

  • 指定复制文件的目的地

但这种表面上的简单性隐藏了丰富的 API,它允许对哪些文件被复制、它们go哪里以及复制时发生的情况进行细粒度控制 - 例如,文件重命名和文件内容的令牌替换都是可能的。

让我们从列表中的最后两项开始,它们构成了所谓的复制规范。这正式基于任务实现的CopySpec接口Copy,并提供:

CopySpec有几个附加方法可以让您控制复制过程,但这两个是唯一需要的。很简单,需要一个目录路径作为其参数,采用Project.file(java.lang.Object)into()方法支持的任何形式。配置更加灵活。from()

不仅from()接受多个参数,还允许多种不同类型的参数。例如,一些最常见的类型是:

  • A String— 视为文件路径,或者如果以“file://”开头,则视为文件 URI

  • A File— 用作文件路径

  • AFileCollectionFileTree— 集合中的所有文件都包含在副本中

  • 任务 -包含形成任务定义输出的文件或目录

事实上,它接受与Project.files(java.lang.Object...​)from()相同的所有参数,因此请参阅该方法以获取可接受类型的更详细列表。

其他需要考虑的是文件路径指的是什么类型:

  • 文件 — 文件按原样复制

  • 目录 — 这实际上被视为文件树:其中的所有内容(包括子目录)都会被复制。但是,目录本身不包含在副本中。

  • 不存在的文件 - 路径被忽略

下面是一个使用多个规范的示例from(),每个规范都有不同的参数类型。您可能还会注意到,它是into()使用闭包(在 Groovy 中)或 Provider(在 Kotlin 中)进行延迟配置的——这种技术也适用于from()

build.gradle.kts
tasks.register<Copy>("anotherCopyTask") {
    // Copy everything under src/main/webapp
    from("src/main/webapp")
    // Copy a single file
    from("src/staging/index.html")
    // Copy the output of a task
    from(copyTask)
    // Copy the output of a task using Task outputs explicitly.
    from(tasks["copyTaskWithPatterns"].outputs)
    // Copy the contents of a Zip file
    from(zipTree("src/main/assets.zip"))
    // Determine the destination directory later
    into({ getDestDir() })
}
build.gradle
tasks.register('anotherCopyTask', Copy) {
    // Copy everything under src/main/webapp
    from 'src/main/webapp'
    // Copy a single file
    from 'src/staging/index.html'
    // Copy the output of a task
    from copyTask
    // Copy the output of a task using Task outputs explicitly.
    from copyTaskWithPatterns.outputs
    // Copy the contents of a Zip file
    from zipTree('src/main/assets.zip')
    // Determine the destination directory later
    into { getDestDir() }
}

请注意, 的惰性配置与子规范into()不同,尽管语法相似。请注意参数的数量以区分它们。

过滤文件

您已经看到,您可以直接在任务中过滤文件集合和文件树,但您也可以通过CopySpec.include(java.lang.String…​)CopySpec.exclude(java.lang.StringCopy )在任何复制规范中应用过滤。lang.String...​)方法。

这两种方法通常与 Ant 风格的包含或排除模式一起使用,如PatternFilterable中所述。您还可以使用闭包来执行更复杂的逻辑,该闭包采用FileTreeElement并返回true是否应包含文件或false其他内容。以下示例演示了这两种形式,确保仅复制 .html 和 .jsp 文件,但内容中包含单词“DRAFT”的 .html 文件除外:

build.gradle.kts
tasks.register<Copy>("copyTaskWithPatterns") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWar"))
    include("**/*.html")
    include("**/*.jsp")
    exclude { details: FileTreeElement ->
        details.file.name.endsWith(".html") &&
            details.file.readText().contains("DRAFT")
    }
}
build.gradle
tasks.register('copyTaskWithPatterns', Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWar')
    include '**/*.html'
    include '**/*.jsp'
    exclude { FileTreeElement details ->
        details.file.name.endsWith('.html') &&
            details.file.text.contains('DRAFT')
    }
}

此时您可能会问自己一个问题:当包含和排除模式重叠时会发生什么?哪种模式获胜?以下是基本规则:

  • 如果没有明确包含或排除,则所有内容都包含在内

  • 如果至少指定了一项包含,则仅包含与模式匹配的文件和目录

  • 任何排除模式都会覆盖所有包含模式,因此,如果文件或目录至少匹配一种排除模式,则无论包含模式如何,都不会包含该文件或目录

创建组合的包含和排除规范时请记住这些规则,以便最终获得您想要的确切行为。

请注意,上例中的包含和排除内容将适用于所有 from()配置。如果您想对复制文件的子集应用过滤,则需要使用子规范

重命名文件

如何重命名副本上的文件的示例为您提供了执行此操作所需的大部分信息。它演示了重命名的两个选项:

  • 使用正则表达式

  • 使用闭包

正则表达式是一种灵活的重命名方法,特别是因为 Gradle 支持正则表达式组,允许您删除和替换部分源文件名。以下示例显示如何使用简单的正则表达式从包含字符串“-staging”的任何文件名中删除该字符串:

build.gradle.kts
tasks.register<Copy>("rename") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWar"))
    // Use a regular expression to map the file name
    rename("(.+)-staging(.+)", "$1$2")
    rename("(.+)-staging(.+)".toRegex().pattern, "$1$2")
    // Use a closure to convert all file names to upper case
    rename { fileName: String ->
        fileName.toUpperCase()
    }
}
build.gradle
tasks.register('rename', Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWar')
    // Use a regular expression to map the file name
    rename '(.+)-staging(.+)', '$1$2'
    rename(/(.+)-staging(.+)/, '$1$2')
    // Use a closure to convert all file names to upper case
    rename { String fileName ->
        fileName.toUpperCase()
    }
}

您可以使用 JavaPattern类支持的任何正则表达式和替换字符串( 的第二个参数rename()与该方法的工作原理相同Matcher.appendReplacement()

Groovy 构建脚本中的正则表达式

在这种情况下使用正则表达式时,人们会遇到两个常见问题:

  1. 如果您使用斜杠字符串(由“/”分隔)作为第一个参数,则必须包含括号,rename()如上例所示。

  2. 对第二个参数使用单引号是最安全的,否则您需要在组替换中转义 '$',即"\$1\$2"

第一个是轻微的不便,但斜杠字符串的优点是您不必在正则表达式中转义反斜杠('\')字符。第二个问题源于 Groovy 对使用${ }双引号和斜杠字符串中的语法的嵌入式表达式的支持。

的闭包语法rename()非常简单,可用于简单正则表达式无法处理的任何要求。您将获得一个文件的名称,然后返回该文件的新名称,或者null如果您不想更改该名称。请注意,将为复制的每个文件执行闭包,因此请尽可能避免昂贵的操作。

过滤文件内容(令牌替换、模板化等)

不要与过滤要复制的文件混淆,文件内容过滤允许您在复制文件时转换文件的内容。这可能涉及使用标记替换的基本模板、删除文本行,甚至使用成熟的模板引擎进行更复杂的过滤。

以下示例演示了多种形式的过滤,包括使用CopySpec.expand(java.util.Map)方法的标记替换以及使用CopySpec.filter(java.lang.Class)Ant 过滤器的另一种方法:

build.gradle.kts
import org.apache.tools.ant.filters.FixCrLfFilter
import org.apache.tools.ant.filters.ReplaceTokens
tasks.register<Copy>("filter") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWar"))
    // Substitute property tokens in files
    expand("copyright" to "2009", "version" to "2.3.1")
    // Use some of the filters provided by Ant
    filter(FixCrLfFilter::class)
    filter(ReplaceTokens::class, "tokens" to mapOf("copyright" to "2009", "version" to "2.3.1"))
    // Use a closure to filter each line
    filter { line: String ->
        "[$line]"
    }
    // Use a closure to remove lines
    filter { line: String ->
        if (line.startsWith('-')) null else line
    }
    filteringCharset = "UTF-8"
}
build.gradle
import org.apache.tools.ant.filters.FixCrLfFilter
import org.apache.tools.ant.filters.ReplaceTokens

tasks.register('filter', Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWar')
    // Substitute property tokens in files
    expand(copyright: '2009', version: '2.3.1')
    // Use some of the filters provided by Ant
    filter(FixCrLfFilter)
    filter(ReplaceTokens, tokens: [copyright: '2009', version: '2.3.1'])
    // Use a closure to filter each line
    filter { String line ->
        "[$line]"
    }
    // Use a closure to remove lines
    filter { String line ->
        line.startsWith('-') ? null : line
    }
    filteringCharset = 'UTF-8'
}

filter()方法有两个变体,其行为不同:

  • 一个采用 a FilterReader,旨在与 Ant 过滤器一起使用,例如ReplaceTokens

  • 一个采用闭包或Transformer来定义源文件每一行的转换

请注意,这两种变体都假设源文件是基于文本的。当您将ReplaceTokens类与 一起使用时filter(),结果是一个模板引擎,它将表单的标记@tokenName@(Ant 样式标记)替换为您定义的值。

expand()方法将源文件视为Groovy 模板,它计算并扩展表单的表达式${expression}。您可以传入属性名称和值,然后在源文件中扩展它们。expand()由于嵌入的表达式是成熟的 Groovy 表达式,因此允许进行的不仅仅是基本的标记替换。

读取和写入文件时最好指定字符集,否则转换将无法正确处理非 ASCII 文本。您可以使用CopySpec.setFilteringCharset(String)属性配置字符集。如果未指定,则使用 JVM 默认字符集,该字符集可能与您想要的不同。

设置文件权限

对于任何CopySpec涉及复制文件的内容,无论是Copy任务本身还是任何子规范,您都可以通过 CopySpec.filePermissions {}配置块显式设置目标文件将具有的权限。您也可以通过CopySpec.dirPermissions {}配置块对目录执行相同的操作,而与文件无关。

不显式设置权限将保留原始文件或目录的权限。
build.gradle.kts
tasks.register<Copy>("permissions") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWar"))
    filePermissions {
        user {
            read = true
            execute = true
        }
        other.execute = false
    }
    dirPermissions {
        unix("r-xr-x---")
    }
}
build.gradle
tasks.register('permissions', Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWar')
    filePermissions {
        user {
            read = true
            execute = true
        }
        other.execute = false
    }
    dirPermissions {
        unix('r-xr-x---')
    }
}

有关文件权限的详细说明,请参阅FilePermissionsUserClassFilePermissions。有关示例中使用的便捷方法的详细信息,请参阅ConfigurableFilePermissions.unix(String)

对文件或目录权限使用空配置块仍然会显式地设置它们,只是固定的默认值。事实上,这些配置块之一内的所有内容都与默认值相关。文件和目录的默认权限不同:

  • 文件:为所有者读取和写入,为读取,为其他读取(0644rw-r—​r--

  • 目录所有者读取、写入和执行,读取和执行,其他读取和执行(0755rwxr-xr-x

使用CopySpec

复制规范(或简称复制规范)决定将什么内容复制到何处,以及在复制过程中文件会发生什么情况。您已经看到了许多配置Copy和归档任务形式的示例。但复制规范有两个属性值得更详细地介绍:

  1. 它们可以独立于任务

  2. 他们是有等级制度的

第一个属性允许您在构建中共享复制规范。第二个在整个复制规范中提供细粒度的控制。

共享复印规格

考虑一个包含多个任务的构建,这些任务复制项目的静态网站资源或将它们添加到存档中。一项任务可能会将资源复制到本地 HTTP 服务器的文件夹中,另一项任务可能会将它们打包到发行版中。您可以在每次需要时手动指定文件位置和适当的包含内容,但人为错误更有可能蔓延,导致任务之间不一致。

Gradle 提供的一种解决方案是Project.copySpec(org.gradle.api.Action)方法。这允许您在任务之外创建复制规范,然后可以使用 CopySpec.with (org.gradle.api.file.CopySpec…​)方法将其附加到适当的任务。以下示例演示了这是如何完成的:

build.gradle.kts
val webAssetsSpec: CopySpec = copySpec {
    from("src/main/webapp")
    include("**/*.html", "**/*.png", "**/*.jpg")
    rename("(.+)-staging(.+)", "$1$2")
}

tasks.register<Copy>("copyAssets") {
    into(layout.buildDirectory.dir("inPlaceApp"))
    with(webAssetsSpec)
}

tasks.register<Zip>("distApp") {
    archiveFileName = "my-app-dist.zip"
    destinationDirectory = layout.buildDirectory.dir("dists")

    from(appClasses)
    with(webAssetsSpec)
}
build.gradle
CopySpec webAssetsSpec = copySpec {
    from 'src/main/webapp'
    include '**/*.html', '**/*.png', '**/*.jpg'
    rename '(.+)-staging(.+)', '$1$2'
}

tasks.register('copyAssets', Copy) {
    into layout.buildDirectory.dir("inPlaceApp")
    with webAssetsSpec
}

tasks.register('distApp', Zip) {
    archiveFileName = 'my-app-dist.zip'
    destinationDirectory = layout.buildDirectory.dir('dists')

    from appClasses
    with webAssetsSpec
}

copyAssets和任务都distApp将处理 下的静态资源src/main/webapp,如 指定的webAssetsSpec

定义的配置webAssetsSpec不适用于distApp任务包含的应用程序类。这是因为from appClasses它自己的子规范独立于with webAssetsSpec.

这可能会令人难以理解,因此最好将其视为任务中的with()额外规范。from()因此,在没有至少from()定义一个副本规范的情况下定义一个独立的副本规范是没有意义的。

如果遇到要将相同的复制配置应用于不同文件集的场景,那么您可以直接共享配置块,而无需使用copySpec().下面是一个示例,其中有两个独立任务恰好只想处理图像文件:

build.gradle.kts
val webAssetPatterns = Action<CopySpec> {
    include("**/*.html", "**/*.png", "**/*.jpg")
}

tasks.register<Copy>("copyAppAssets") {
    into(layout.buildDirectory.dir("inPlaceApp"))
    from("src/main/webapp", webAssetPatterns)
}

tasks.register<Zip>("archiveDistAssets") {
    archiveFileName = "distribution-assets.zip"
    destinationDirectory = layout.buildDirectory.dir("dists")

    from("distResources", webAssetPatterns)
}
build.gradle
def webAssetPatterns = {
    include '**/*.html', '**/*.png', '**/*.jpg'
}

tasks.register('copyAppAssets', Copy) {
    into layout.buildDirectory.dir("inPlaceApp")
    from 'src/main/webapp', webAssetPatterns
}

tasks.register('archiveDistAssets', Zip) {
    archiveFileName = 'distribution-assets.zip'
    destinationDirectory = layout.buildDirectory.dir('dists')

    from 'distResources', webAssetPatterns
}

在这种情况下,我们将复制配置分配给它自己的变量,并将其应用于from()我们想要的任何规范。这不仅适用于包含,还适用于排除、文件重命名和文件内容过滤。

使用子规范

如果您仅使用单个复制规范,则文件过滤和重命名将应用于所有复制的文件。有时这就是您想要的,但并非总是如此。考虑以下示例,该示例将文件复制到可供 Java Servlet 容器用来交付网站的目录结构中:

爆炸战争子副本规范示例
图 4. 为 Servlet 容器创建分解的 WAR

这不是一个简单的复制,因为WEB-INF项目中不存在该目录及其子目录,因此必须在复制过程中创建它们。此外,我们只希望 HTML 和图像文件直接进入根文件夹,build/explodedWar并且只希望 JavaScript 文件进入该js目录。因此,我们需要为这两组文件提供单独的过滤模式。

解决方案是使用子规范,它可以应用于from()into()声明。以下任务定义完成了必要的工作:

Example 39. Nested copy specs
build.gradle.kts
tasks.register<Copy>("nestedSpecs") {
    into(layout.buildDirectory.dir("explodedWar"))
    exclude("**/*staging*")
    from("src/dist") {
        include("**/*.html", "**/*.png", "**/*.jpg")
    }
    from(sourceSets.main.get().output) {
        into("WEB-INF/classes")
    }
    into("WEB-INF/lib") {
        from(configurations.runtimeClasspath)
    }
}
build.gradle
tasks.register('nestedSpecs', Copy) {
    into layout.buildDirectory.dir("explodedWar")
    exclude '**/*staging*'
    from('src/dist') {
        include '**/*.html', '**/*.png', '**/*.jpg'
    }
    from(sourceSets.main.output) {
        into 'WEB-INF/classes'
    }
    into('WEB-INF/lib') {
        from configurations.runtimeClasspath
    }
}

请注意配置如何src/dist具有嵌套包含规范:这就是子副本规范。您当然可以根据需要在此处添加内容过滤和重命名。子副本规范仍然是副本规范。

上面的示例还演示了如何通过使用into()a 上的子目录from()或.这两种方法都是可以接受的,但您可能需要创建并遵循约定以确保构建文件之间的一致性。from()into()

不要into()混淆您的规格! ] 对于普通副本(复制到文件系统而不是存档),应该始终有一个“根”into()来简单地指定副本的整个目标目录。任何其他都into()应该附加一个子规范,并且其路径将相对于 root into()

最后要注意的一件事是,子复制规范从其父级继承其目标路径,包括模式、排除模式、复制操作、名称映射和过滤器。因此要小心放置配置的位置。

在您自己的任务中复制文件

Project.copy如此处所述,在执行时 使用该方法与配置缓存不兼容。一种可能的解决方案是将任务实现为适当的类,并使用FileSystemOperations.copy方法代替,如配置缓存章节中所述。

有时您可能想要复制文件或目录作为任务的一部分。例如,基于不受支持的存档格式的自定义存档任务可能需要在存档之前将文件复制到临时目录。您仍然想利用 Gradle 的复制 API,但不想引入额外的Copy任务。

解决方案是使用Project.copy(org.gradle.api.Action)方法。它的工作方式与Copy任务相同,通过使用复制规范对其进行配置。这是一个简单的例子:

build.gradle.kts
tasks.register("copyMethod") {
    doLast {
        copy {
            from("src/main/webapp")
            into(layout.buildDirectory.dir("explodedWar"))
            include("**/*.html")
            include("**/*.jsp")
        }
    }
}
build.gradle
tasks.register('copyMethod') {
    doLast {
        copy {
            from 'src/main/webapp'
            into layout.buildDirectory.dir('explodedWar')
            include '**/*.html'
            include '**/*.jsp'
        }
    }
}

上面的示例演示了基本语法,并强调了使用该copy()方法的两个主要限制:

  1. copy()方法不是渐进的。该示例的copyMethod任务将始终执行,因为它没有有关哪些文件构成任务输入的信息。您必须手动定义任务输入和输出。

  2. 使用任务作为复制源,即作为 的参数from(),不会在您的任务和该复制源之间设置自动任务依赖关系。因此,如果您将该copy()方法用作任务操作的一部分,则必须显式声明所有输入和输出以获得正确的行为。

以下示例向您展示如何通过使用任务输入和输出的动态 API来解决这些限制:

build.gradle.kts
tasks.register("copyMethodWithExplicitDependencies") {
    // up-to-date check for inputs, plus add copyTask as dependency
    inputs.files(copyTask)
        .withPropertyName("inputs")
        .withPathSensitivity(PathSensitivity.RELATIVE)
    outputs.dir("some-dir") // up-to-date check for outputs
        .withPropertyName("outputDir")
    doLast {
        copy {
            // Copy the output of copyTask
            from(copyTask)
            into("some-dir")
        }
    }
}
build.gradle
tasks.register('copyMethodWithExplicitDependencies') {
    // up-to-date check for inputs, plus add copyTask as dependency
    inputs.files(copyTask)
        .withPropertyName("inputs")
        .withPathSensitivity(PathSensitivity.RELATIVE)
    outputs.dir('some-dir') // up-to-date check for outputs
        .withPropertyName("outputDir")
    doLast {
        copy {
            // Copy the output of copyTask
            from copyTask
            into 'some-dir'
        }
    }
}

这些限制使得Copy尽可能使用该任务更好,因为它内置支持增量构建和任务依赖性推断。这就是为什么该copy()方法旨在供需要复制文件作为其功能一部分的自定义任务使用。使用该方法的自定义任务copy()应声明与复制操作相关的必要输入和输出。

Sync使用任务镜像目录和文件集合

同步任务扩展了该任务Copy,将源文件复制到目标目录,然后从目标目录中删除未复制的所有文件。换句话说,它将目录的内容与其源同步。这对于执行诸如安装应用程序、创建存档的分解副本或维护项目依赖项的副本等操作非常有用。

下面是一个在目录中维护项目运行时依赖项的副本的示例build/libs

build.gradle.kts
tasks.register<Sync>("libs") {
    from(configurations["runtime"])
    into(layout.buildDirectory.dir("libs"))
}
build.gradle
tasks.register('libs', Sync) {
    from configurations.runtime
    into layout.buildDirectory.dir('libs')
}

您还可以使用Project.sync(org.gradle.api.Action)方法在自己的任务中执行相同的功能。

将单个文件部署到应用程序服务器中

使用应用程序服务器时,您可以使用Copy任务来部署应用程序存档(例如 WAR 文件)。由于您正在部署单个文件,因此该文件的目标目录Copy是整个部署目录。部署目录有时确实包含不可读的文件,例如命名管道,因此 Gradle 可能在进行最新检查时遇到问题。为了支持此用例,您可以使用Task.doNotTrackState()

build.gradle.kts
plugins {
    war
}

tasks.register<Copy>("deployToTomcat") {
    from(tasks.war)
    into(layout.projectDirectory.dir("tomcat/webapps"))
    doNotTrackState("Deployment directory contains unreadable files")
}
build.gradle
plugins {
    id 'war'
}

tasks.register("deployToTomcat", Copy) {
    from war
    into layout.projectDirectory.dir('tomcat/webapps')
    doNotTrackState("Deployment directory contains unreadable files")
}

安装可执行文件

当您构建独立的可执行文件时,您可能希望在系统上安装此文件,因此它最终会出现在您的路径中。您可以使用Copy任务将可执行文件安装到共享目录中,例如/usr/local/bin.安装目录可能包含许多其他可执行文件,其中一些甚至可能无法被 Gradle 读取。为了支持任务目标目录中的不可读文件Copy并避免耗时的最新检查,您可以使用Task.doNotTrackState()

build.gradle.kts
tasks.register<Copy>("installExecutable") {
    from("build/my-binary")
    into("/usr/local/bin")
    doNotTrackState("Installation directory contains unrelated files")
}
build.gradle
tasks.register("installExecutable", Copy) {
    from "build/my-binary"
    into "/usr/local/bin"
    doNotTrackState("Installation directory contains unrelated files")
}

深入档案创建

档案本质上是独立的文件系统,Gradle 也这样对待它们。这就是为什么处理档案与处理文件和目录非常相似,包括文件权限之类的事情。

Gradle 开箱即用,支持创建 ZIP 和 TAR 存档,并扩展了 Java 的 JAR、WAR 和 EAR 格式——Java 的存档格式都是 ZIP。每种格式都有相应的任务类型来创建它们:ZipTarJarWarEar。这些都以相同的方式工作,并且基于副本规范,就像Copy任务一样。

创建归档文件本质上是一个文件副本,其中目标是隐式的,即归档文件本身。下面是一个指定目标存档文件的路径和名称的基本示例:

build.gradle.kts
tasks.register<Zip>("packageDistribution") {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = layout.buildDirectory.dir("dist")

    from(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('packageDistribution', Zip) {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = layout.buildDirectory.dir('dist')

    from layout.buildDirectory.dir("toArchive")
}

在下一节中,您将了解基于约定的存档名称,这可以使您免于总是配置目标目录和存档名称。

创建存档时,您可以使用复制规范的全部功能,这意味着您可以执行内容过滤、文件重命名或上一节中介绍的任何其他操作。一个特别常见的要求是将文件复制到源文件夹中不存在的存档子目录中,这可以通过into() 子规范来实现。

Gradle 当然允许您创建任意数量的存档任务,但值得记住的是,许多基于约定的插件都提供了自己的任务。例如,Java 插件添加了一个jar任务,用于将项目的已编译类和资源打包到 JAR 中。其中许多插件为档案名称以及所使用的复制规范提供了合理的约定。我们建议您尽可能使用这些任务,而不是用自己的任务覆盖它们。

档案命名

Gradle 对于档案的命名以及根据项目使用的插件创建档案的位置有多种约定。主要约定由Base Plugin提供,它默认在目录中创建存档,并且通常使用[projectName]-[version].[type]layout.buildDirectory.dir("distributions")形式的存档名称。

以下示例来自名为 的项目archive-naming,因此该myZip任务创建一个名为 的存档archive-naming-1.0.zip

build.gradle.kts
plugins {
    base
}

version = "1.0"

tasks.register<Zip>("myZip") {
    from("somedir")
    val projectDir = layout.projectDirectory.asFile
    doLast {
        println(archiveFileName.get())
        println(destinationDirectory.get().asFile.relativeTo(projectDir))
        println(archiveFile.get().asFile.relativeTo(projectDir))
    }
}
build.gradle
plugins {
    id 'base'
}

version = 1.0

tasks.register('myZip', Zip) {
    from 'somedir'
    File projectDir = layout.projectDirectory.asFile
    doLast {
        println archiveFileName.get()
        println projectDir.relativePath(destinationDirectory.get().asFile)
        println projectDir.relativePath(archiveFile.get().asFile)
    }
}
输出gradle -q myZip
> gradle -q myZip
archive-naming-1.0.zip
build/distributions
build/distributions/archive-naming-1.0.zip

请注意,存档的名称并非源自创建它的任务的名称。

如果要更改生成的存档文件的名称和位置,可以为相应任务的archiveFileName和属性提供值。destinationDirectory这些规则会覆盖任何原本适用的约定。

或者,您可以使用AbstractArchiveTask.getArchiveFileName()提供的默认存档名称模式:[archiveBaseName]-[archiveAppendix]-[archiveVersion]-[archiveClassifier].[archiveExtension]。如果您愿意,您可以单独设置任务的每个属性。请注意,基础插件使用以下约定:archiveBaseName为项目名称、 archiveVersion为项目版本、 archiveExtension为存档类型。它不提供其他属性的值。

此示例(与上面的项目来自同一项目)仅配置属性archiveBaseName,覆盖项目名称的默认值:

build.gradle.kts
tasks.register<Zip>("myCustomZip") {
    archiveBaseName = "customName"
    from("somedir")

    doLast {
        println(archiveFileName.get())
    }
}
build.gradle
tasks.register('myCustomZip', Zip) {
    archiveBaseName = 'customName'
    from 'somedir'

    doLast {
        println archiveFileName.get()
    }
}
输出gradle -q myCustomZip
> gradle -q myCustomZip
customName-1.0.zip

您还可以使用项目属性覆盖构建中所有archiveBaseName存档任务的默认值,如以下示例所示:archivesBaseName

build.gradle.kts
plugins {
    base
}

version = "1.0"

base {
    archivesName = "gradle"
    distsDirectory = layout.buildDirectory.dir("custom-dist")
    libsDirectory = layout.buildDirectory.dir("custom-libs")
}

val myZip by tasks.registering(Zip::class) {
    from("somedir")
}

val myOtherZip by tasks.registering(Zip::class) {
    archiveAppendix = "wrapper"
    archiveClassifier = "src"
    from("somedir")
}

tasks.register("echoNames") {
    val projectNameString = project.name
    val archiveFileName = myZip.flatMap { it.archiveFileName }
    val myOtherArchiveFileName = myOtherZip.flatMap { it.archiveFileName }
    doLast {
        println("Project name: $projectNameString")
        println(archiveFileName.get())
        println(myOtherArchiveFileName.get())
    }
}
build.gradle
plugins {
    id 'base'
}

version = 1.0
base {
    archivesName = "gradle"
    distsDirectory = layout.buildDirectory.dir('custom-dist')
    libsDirectory = layout.buildDirectory.dir('custom-libs')
}

def myZip = tasks.register('myZip', Zip) {
    from 'somedir'
}

def myOtherZip = tasks.register('myOtherZip', Zip) {
    archiveAppendix = 'wrapper'
    archiveClassifier = 'src'
    from 'somedir'
}

tasks.register('echoNames') {
    def projectNameString = project.name
    def archiveFileName = myZip.flatMap { it.archiveFileName }
    def myOtherArchiveFileName = myOtherZip.flatMap { it.archiveFileName }
    doLast {
        println "Project name: $projectNameString"
        println archiveFileName.get()
        println myOtherArchiveFileName.get()
    }
}
输出gradle -q echoNames
> gradle -q echoNames
Project name: archives-changed-base-name
gradle-1.0.zip
gradle-wrapper-1.0-src.zip

您可以在AbstractArchiveTask的 API 文档中找到所有可能的归档任务属性,但我们还在这里总结了主要属性:

archiveFileName- Property<String>, 默认:archiveBaseName-archiveAppendix-archiveVersion-archiveClassifier.archiveExtension

生成的存档的完整文件名。如果默认值中的任何属性为空,则它们的“-”分隔符将被删除。

archiveFileProvider<RegularFile>只读,默认:destinationDirectory/archiveFileName

生成的存档的绝对文件路径。

destinationDirectoryDirectoryProperty,默认值:取决于存档类型

放置生成的存档的目标目录。默认情况下,JAR 和 WAR 进入layout.buildDirectory.dir("libs"). ZIP 和 TAR 进入layout.buildDirectory.dir("distributions").

archiveBaseName- Property<String>, 默认:project.name

存档文件名的基本名称部分,通常是项目名称或它所包含内容的其他描述性名称。

archiveAppendix- Property<String>, 默认:null

紧随基本名称之后的存档文件名的附录部分。它通常用于区分不同形式的内容,例如代码和文档,或者最小分发与完整分发。

archiveVersion- Property<String>, 默认:project.version

存档文件名的版本部分,通常采用正常项目或产品版本的形式。

archiveClassifier- Property<String>, 默认:null

归档文件名的分类器部分。通常用于区分针对不同平台的档案。

archiveExtensionProperty<String>,默认值:取决于存档类型和压缩类型

存档的文件扩展名。默认情况下,这是根据存档任务类型和压缩类型(如果您正在创建 TAR)进行设置的。将是以下之一:zipjarwartartgztbz2如果您愿意,您当然可以将其设置为自定义扩展。

在多个档案之间共享内容

如前所述,您可以使用Project.copySpec(org.gradle.api.Action)方法在存档之间共享内容。

可重复的构建

有时需要在不同的机器上逐字节地重新创建完全相同的存档。您希望确保从源代码构建工件无论何时何地构建都会产生相同的结果。这对于像reproducible-builds.org这样的项目是必要的。

逐字节复制相同的存档会带来一些挑战,因为存档中文件的顺序受到底层文件系统的影响。每次从源代码构建 ZIP、TAR、JAR、WAR 或 EAR 时,存档内文件的顺序可能会发生变化。仅具有不同时间戳的文件也会导致构建与构建之间的存档存在差异。Gradle 附带的所有AbstractArchiveTask (例如 Jar、Zip)任务都包含对生成可重现档案的支持。

例如,要使Zip任务可重复,您需要将Zip.isReproducibleFileOrder()设置为true并将Zip.isPreserveFileTimestamps()设置为false。为了使构建中的所有存档任务可重现,请考虑将以下配置添加到构建文件中:

build.gradle.kts
tasks.withType<AbstractArchiveTask>().configureEach {
    isPreserveFileTimestamps = false
    isReproducibleFileOrder = true
}
build.gradle
tasks.withType(AbstractArchiveTask).configureEach {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}

通常,您会想要发布存档,以便可以在其他项目中使用它。跨项目出版物中描述了此过程。