几乎每个 Gradle 构建都会以某种方式与文件交互:想想源文件、文件依赖项、报告等等。这就是 Gradle 附带一个全面 API 的原因,它可以简化执行所需的文件操作。

API 有两个部分

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

  • 指定对它们执行的操作

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

复制单个文件

您可以通过创建 Gradle 内置 Copy 任务的实例并使用文件的位置和要放置文件的位置对其进行配置来复制文件。此示例模拟将生成的报告复制到将打包到存档(如 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 才会被复制

copy with flat filter example
图 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")
}

此任务具有以下效果

copy with deep filter example
图 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 的角度来看,将文件打包到存档中实际上是一种复制,其中目标是存档文件,而不是文件系统上的目录。这意味着创建存档看起来很像复制,具有所有相同的功能!

最简单的情况涉及存档目录的全部内容,此示例通过创建 toArchive 目录的 ZIP 来演示这一点

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():两者都是必需的。您通常不会看到它们被明确设置,因为大多数项目都应用了 基本插件。它为这些属性提供了一些约定值。下一个示例演示了这一点,您可以在 存档命名 部分中了解有关约定的更多信息。

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

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

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) 方法和 Jar 任务,如下面的示例中 uberJar 任务所示

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 — 并使用 zipTree() 方法包装每个 JAR 文件。结果是 ZIP 文件树的集合,其内容与应用程序类一起复制到 uber JAR 中。

创建目录

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

在需要手动创建目录的情况下,您可以在构建脚本或自定义任务实现中使用标准的Files.createDirectoriesFile.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()的子规范中对其进行配置,将重命名应用于文件子集。

删除文件和目录

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

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

示例 17. 删除目录
build.gradle.kts
tasks.register<Delete>("myClean") {
    delete(buildDir)
}
build.gradle
tasks.register('myClean', Delete) {
    delete buildDir
}

如果您希望更好地控制要删除的文件,则不能像复制文件那样使用包含和排除。相反,您必须使用FileCollectionFileTree的内置过滤机制。以下示例就是这样做的,以清除源目录中的临时文件

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 构建基于标准 Java File 类,该类表示单个文件的位置,并提供了用于处理路径集合的新 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) 方法来指定单个文件或目录的位置。相对路径相对于项目目录解析,而绝对路径保持不变。

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

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

示例 20. 定位文件
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 目录中处理多项目构建。您在要修复的库的构建中使用了上面的示例,位于 AcmeHealth/subprojects/AcmePatientRecordLib/build.gradle。文件路径将解析为 dev/projects/AcmeHealth/shared/config.xml 的绝对版本。

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

文件集合

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

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

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

Project.file(java.lang.Object) 方法类似(在 上一节 中介绍),所有相对路径均相对于当前项目目录进行评估。以下示例演示了可使用的各种参数类型 — 字符串、File 实例、列表和 Path

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

延迟创建的关键是向 files() 方法传递闭包(在 Groovy 中)或 Provider(在 Kotlin 中)。闭包/提供程序只需返回 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')

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

例如,假设在创建 union 之后,上述示例中的 collection 获得了一个或两个额外文件。只要在将这些文件添加到 collection 之后使用 unionunion 也将包含这些附加文件。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() 的闭包将 File 作为参数,并应返回一个布尔值。

文件树

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

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

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

示例 26. 创建文件树
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 文档中看到更多受支持模式的示例。此外,请参阅 fileTree() 的 API 文档,以查看你可以将哪些类型作为基本目录传递。

默认情况下,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) 方法遍历文件树。以下示例演示了所有这些技术

示例 28. 使用文件树
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 属性,用于定义要编译的源文件。如 API 文档中所述,可以使用 files() 方法支持的任何类型设置此属性的值。这意味着你可以将属性设置为 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 中复制文件的基本过程很简单

  • 定义 Copy 类型的任务

  • 指定要复制的文件(和潜在的目录)

  • 为复制的文件指定目标

但这种明显的简单性隐藏了一个丰富的 API,它允许精细地控制哪些文件被复制、它们被复制到哪里以及在复制过程中对它们发生了什么——例如,重命名文件和令牌替换文件内容都是可能的。

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

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

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

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

  • 一个 File — 用作文件路径

  • 一个 FileCollectionFileTree — 集合中的所有文件都包含在复制中

  • 一个任务 — 包含任务的 已定义输出 的文件或目录

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

还需要考虑的是文件路径所指的事物的类型

  • 一个文件 — 文件按原样复制

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

  • 不存在的文件 — 忽略路径

以下示例使用多个 from() 规范,每个规范具有不同的参数类型。您可能还会注意到,into() 使用闭包(在 Groovy 中)或提供程序(在 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() 的延迟配置不同于 子规范,即使语法相似。注意参数数量以区分它们。

过滤文件

您已经看到,您可以在 Copy 任务中直接过滤文件集合和文件树,但您还可以在任何复制规范中通过 CopySpec.include(java.lang.String…​)CopySpec.exclude(java.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()
    }
}

您可以使用 Java Pattern 类支持的任何正则表达式,并且替换字符串(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() 方法有两种变体,行为不同

  • 一种采用 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
}

copyAssetsdistApp 任务都将处理 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 容器可用于提供网站的目录结构中

exploded war child copy spec example
图 4. 为 Servlet 容器创建 exploded WAR

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

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

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 配置具有嵌套包含规范:那是子复制规范。当然,您可以在此处根据需要添加内容筛选和重命名。子复制规范仍然是一个复制规范。

以上示例还演示了如何将文件复制到目标子目录,方法是在 from() 上使用子 into() 或在 into() 上使用子 from()。两种方法都是可以接受的,但您可能希望创建并遵循约定,以确保构建文件的一致性。

不要混淆您的 into() 规范!] 对于普通复制 — 到文件系统而不是归档 — 应该始终有一个一个“根”into(),它只指定复制的总体目标目录。任何其他 into() 都应该附加一个子规范,其路径将相对于根 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 任务的扩展 Sync 任务将源文件复制到目标目录,然后从目标目录中删除其未复制的任何文件。换句话说,它将目录的内容与其源同步。这对于执行诸如安装应用程序、创建存档的展开副本或维护项目依赖项副本等操作非常有用。

这里有一个示例,它维护 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 插件 提供,该插件默认为在 layout.buildDirectory.dir("distributions") 目录中创建存档,并且通常使用格式为 [projectName]-[version].[type] 的存档名称。

以下示例来自名为 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

请注意,存档的名称不会派生自创建它的任务的名称。

如果您想更改生成存档文件的名称和位置,则可以为相应任务的 archiveFileNamedestinationDirectory 属性提供值。这些值会覆盖任何其他适用的约定。

或者,您可以使用 AbstractArchiveTask.getArchiveFileName() 提供的默认存档名称模式:[archiveBaseName]-[archiveAppendix]-[archiveVersion]-[archiveClassifier].[archiveExtension]。如果您愿意,可以在任务上分别设置这些属性。请注意,Base 插件使用项目名称的约定作为 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

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

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 文档中找到所有可能的归档任务属性,但我们在此也总结了主要属性

archiveFileNameProperty<String>,默认值:archiveBaseName-archiveAppendix-archiveVersion-archiveClassifier.archiveExtension

生成归档的完整文件名。如果默认值中的任何属性为空,则会删除其 '-' 分隔符。

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

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

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

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

archiveBaseNameProperty<String>,默认值:project.name

归档文件名中的基本名称部分,通常是项目名称或对其包含内容的一些其他描述性名称。

archiveAppendixProperty<String>,默认值:null

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

archiveVersionProperty<String>,默认值:project.version

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

archiveClassifierProperty<String>,默认值:null

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

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

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

在多个存档之间共享内容

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

可再现构建

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

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

例如,要使 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
}

通常,你希望发布存档,以便它可以从另一个项目中使用。此过程在 跨项目发布 中进行描述。