几乎每个Gradle构建都以某种方式与文件交互:考虑源文件,文件依赖项,报告等. 这就是为什么Gradle带有一个全面的API,该API使执行所需的文件操作变得简单.

API包含两个部分:

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

  • 指定如何处理它们

" 深度文件路径"部分详细介绍了第一个,而后续部分(例如" 深度文件复制" )覆盖了第二个. 首先,我们将向您展示用户遇到的最常见情况的示例.

Copying a single file

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

例子1.如何复制一个文件
build.gradle
task copyReport(type: Copy) {
    from file("$buildDir/reports/my-report.pdf")
    into file("$buildDir/toArchive")
}
build.gradle.kts
tasks.register<Copy>("copyReport") {
    from(file("$buildDir/reports/my-report.pdf"))
    into(file("$buildDir/toArchive"))
}

Project.file(java.lang.Object)方法用于创建相对于当前项目的文件或目录路径,并且是使构建脚本不管项目路径如何都可以工作的常用方法. 文件和目录路径,然后用于指定哪些文件使用复制Copy.from(java.lang.Object中...)和目录复制到使用Copy.into(java.lang.Object中) .

您甚至可以直接使用路径而无需使用file()方法,如在文件深度复制一节中前面所述:

例子2.使用隐式字符串路径
build.gradle
task copyReport2(type: Copy) {
    from "$buildDir/reports/my-report.pdf"
    into "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Copy>("copyReport2") {
    from("$buildDir/reports/my-report.pdf")
    into("$buildDir/toArchive")
}

尽管硬编码路径仅举几个简单的例子,但它们也使构建变脆. 最好使用可靠的单一事实来源,例如任务或共享项目属性. 在下面的修改示例中,我们使用在其他位置定义的报表任务,该报表的位置存储在其outputFile属性中:

例子3.优先于硬编码路径的任务/项目属性
build.gradle
task copyReport3(type: Copy) {
    from myReportTask.outputFile
    into archiveReportsTask.dirToArchive
}
build.gradle.kts
tasks.register<Copy>("copyReport3") {
    val outputFile: File by myReportTask.get().extra
    val dirToArchive: File by archiveReportsTask.get().extra
    from(outputFile)
    into(dirToArchive)
}

我们还假设报告将由archiveReportsTask存档,该存档为我们提供了将要存档的目录,因此我们希望将报告的副本放置在该目录中.

Copying multiple files

您可以通过为from()提供多个参数,很容易地将前面的示例扩展到多个文件:

例子4.与from()一起使用多个参数
build.gradle
task copyReportsForArchiving(type: Copy) {
    from "$buildDir/reports/my-report.pdf", "src/docs/manual.pdf"
    into "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Copy>("copyReportsForArchiving") {
    from("$buildDir/reports/my-report.pdf", "src/docs/manual.pdf")
    into("$buildDir/toArchive")
}

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

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

例子5.使用平面滤波器
build.gradle
task copyPdfReportsForArchiving(type: Copy) {
    from "$buildDir/reports"
    include "*.pdf"
    into "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Copy>("copyPdfReportsForArchiving") {
    from("$buildDir/reports")
    include("*.pdf")
    into("$buildDir/toArchive")
}

如下图所示,要注意的一件事是仅复制了直接位于reports目录中的PDF:

copy with flat filter example
图1.平面过滤器对复制的影响

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

例子6.使用深层过滤器
build.gradle
task copyAllPdfReportsForArchiving(type: Copy) {
    from "$buildDir/reports"
    include "**/*.pdf"
    into "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Copy>("copyAllPdfReportsForArchiving") {
    from("$buildDir/reports")
    include("**/*.pdf")
    into("$buildDir/toArchive")
}

此任务具有以下效果:

copy with deep filter example
图2.深层过滤器对复制的影响

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

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

Copying directory hierarchies

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

例子7.复制整个目录
build.gradle
task copyReportsDirForArchiving(type: Copy) {
    from "$buildDir/reports"
    into "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Copy>("copyReportsDirForArchiving") {
    from("$buildDir/reports")
    into("$buildDir/toArchive")
}

用户苦苦挣扎的关键方面是控制有多少目录结构到达目的地. 在上面的例子中,你得到一个toArchive/reports目录还是全部都在reports直行进入toArchive ? 答案是后者. 如果目录是from()路径的一部分,则它不会出现在目标位置.

因此,如何确保reports本身被复制,但不能复制$buildDir任何其他目录? 答案是将其添加为包含模式:

例子8.复制整个目录,包括它本身
build.gradle
task copyReportsDirForArchiving2(type: Copy) {
    from("$buildDir") {
        include "reports/**"
    }
    into "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Copy>("copyReportsDirForArchiving2") {
    from("$buildDir") {
        include("reports/**")
    }
    into("$buildDir/toArchive")
}

您将获得与以前相同的行为,除了目标中有一个额外的目录级别,即toArchive/reports .

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

Creating archives (zip, tar, etc.)

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

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

例子9.将目录归档为ZIP
build.gradle
task packageDistribution(type: Zip) {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = file("$buildDir/dist")

    from "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Zip>("packageDistribution") {
    archiveFileName.set("my-distribution.zip")
    destinationDirectory.set(file("$buildDir/dist"))

    from("$buildDir/toArchive")
}

请注意,我们如何指定归档文件的目的地和名称,而不是into() :两者都是必需的. 您常常看不到它们的显式设置,因为大多数项目都使用Base Plugin . 它为这些属性提供了一些常规值. 下一个示例对此进行了演示,您可以在归档命名部分中了解有关约定的更多信息.

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

最常见的情况之一是将文件复制到存档的指定子目录中. 例如,假设您要将所有PDF打包到归档文件根目录中的docs目录中. 该docs目录在源位置中不存在,因此您必须将其创建为存档的一部分. 为此,只需为PDF添加一个into()声明:

例子10.使用基本插件为其存档名称约定
build.gradle
plugins {
    id 'base'
}

version = "1.0.0"

task packageDistribution(type: Zip) {
    from("$buildDir/toArchive") {
        exclude "**/*.pdf"
    }

    from("$buildDir/toArchive") {
        include "**/*.pdf"
        into "docs"
    }
}
build.gradle.kts
plugins {
    base
}

version = "1.0.0"

tasks.register<Zip>("packageDistribution") {
    from("$buildDir/toArchive") {
        exclude("**/*.pdf")
    }

    from("$buildDir/toArchive") {
        include("**/*.pdf")
        into("docs")
    }
}

如您所见,复制规范中可以有多个from()声明,每个声明都有自己的配置. 有关此功能的更多信息,请参见使用子副本规范 .

Unpacking archives

档案实际上是独立的文件系统,因此解压缩它们是将文件从该文件系统复制到本地文件系统,甚至复制到另一个档案的情况. \ Gradle通过提供一些包装器功能来实现此目的,这些包装器功能使档案可以作为文件( 文件树 )的分层集合使用.

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

例子11.解压缩一个ZIP文件
build.gradle
task unpackFiles(type: Copy) {
    from zipTree("src/resources/thirdPartyResources.zip")
    into "$buildDir/resources"
}
build.gradle.kts
tasks.register<Copy>("unpackFiles") {
    from(zipTree("src/resources/thirdPartyResources.zip"))
    into("$buildDir/resources")
}

As with a normal copy, you can control which files are unpacked via filters and even rename files as they are unpacked.

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

例子12.解压缩一个ZIP文件的子集
build.gradle
task unpackLibsDirectory(type: 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 "$buildDir/resources"
}
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("$buildDir/resources")
}
  1. 仅提取驻留在libs目录中的文件的子集

  2. 通过从文件路径中删除libs段,将提取文件的路径重新映射到目标目录中

  3. 忽略由于重新映射而导致的空目录,请参阅下面的注意事项

您不能使用此技术更改空目录的目标路径. 您可以在本期中了解更多信息.

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

Creating "uber" or "fat" JARs

在Java空间中,通常将应用程序及其依赖项打包为一个分发归档文件中的独立JAR. 仍然会发生这种情况,但是现在有另一种常见的方法:将依赖项的类和资源直接放入应用程序JAR中,创建所谓的超级或胖JAR.

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

Example 13. Creating a Java uber or fat JAR
build.gradle
plugins {
    id 'java'
}

version = '1.0.0'

repositories {
    mavenCentral()
}

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

task uberJar(type: Jar) {
    archiveClassifier = 'uber'

    from sourceSets.main.output

    dependsOn configurations.runtimeClasspath
    from {
        configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
    }
}
build.gradle.kts
plugins {
    java
}

version = "1.0.0"

repositories {
    mavenCentral()
}

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

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

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

    dependsOn(configurations.runtimeClasspath)
    from({
        configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
    })
}

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

Creating directories

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

如果需要手动创建目录,则可以在构建脚本或自定义任务实现中使用Project.mkdir(java.lang.Object)方法. 这是一个简单的示例,在项目文件夹中创建一个images目录:

例子14.手动创建目录
build.gradle
task ensureDirectory {
    doLast {
        mkdir "images"
    }
}
build.gradle.kts
tasks.register("ensureDirectory") {
    doLast {
        mkdir("images")
    }
}

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

Moving files and directories

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

例15.使用Ant任务移动目录
build.gradle
task moveReports {
    doLast {
        ant.move file: "${buildDir}/reports",
                 todir: "${buildDir}/toArchive"
    }
}
build.gradle.kts
tasks.register("moveReports") {
    doLast {
        ant.withGroovyBuilder {
            "move"("file" to "${buildDir}/reports", "todir" to "${buildDir}/toArchive")
        }
    }
}

这不是常见的要求,在丢失信息并可能容易破坏构建时应谨慎使用. 通常,最好是复制目录和文件.

Renaming files on copy

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

下面的示例从任何包含它的文件的名称中删除" -staging-"标记:

例子16.在复制文件时重命名
build.gradle
task copyFromStaging(type: Copy) {
    from "src/main/webapp"
    into "$buildDir/explodedWar"

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

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

您可以为此使用正则表达式,如上面的示例所示,也可以使用使用更复杂的逻辑来确定目标文件名的闭包. 例如,以下任务将截断文件名:

例子17.在复制文件名时截断它们
build.gradle
task copyWithTruncate(type: Copy) {
    from "$buildDir/reports"
    rename { String filename ->
        if (filename.size() > 10) {
            return filename[0..7] + "~" + filename.size()
        }
        else return filename
    }
    into "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Copy>("copyWithTruncate") {
    from("$buildDir/reports")
    rename { filename: String ->
        if (filename.length > 10) {
            filename.slice(0..7) + "~" + filename.length
        }
        else filename
    }
    into("$buildDir/toArchive")
}

与过滤一样,您也可以通过将重命名配置为from()的子规范的一部分,从而对文件的子集应用重命名.

Deleting files and directories

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

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

例子18.删除目录
build.gradle
task myClean(type: Delete) {
    delete buildDir
}
build.gradle.kts
tasks.register<Delete>("myClean") {
    delete(buildDir)
}

如果要对删除哪些文件有更多控制,则不能以与复制文件相同的方式使用包含和排除. 相反,您必须使用FileCollectionFileTree的内置过滤机制. 以下示例仅用于清除源目录中的临时文件:

例子19.删除匹配特定模式的文件
build.gradle
task cleanTempFiles(type: Delete) {
    delete fileTree("src").matching {
        include "**/*.tmp"
    }
}
build.gradle.kts
tasks.register<Delete>("cleanTempFiles") {
    delete(fileTree("src").matching {
        include("**/*.tmp")
    })
}

在下一部分中,您将了解有关文件集合和文件树的更多信息.

File paths in depth

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

但首先,重要的是有关在构建中使用硬编码的文件路径的注意事项.

On hard-coded file paths

本章中的许多示例都将硬编码路径用作字符串文字. 这使它们易于理解,但是对于实际构建而言,这不是一个好习惯. 问题在于路径经常更改,并且您需要更改的位置越多,您越有可能错过其中一个并破坏构建.

在可能的情况下,应使用任务,任务属性和项目属性 (按优先顺序)来配置文件路径. 例如,如果要创建一个打包Java应用程序的已编译类的任务,则应针对以下目标:

例子20.如何减少构建中硬编码路径的数量
build.gradle
ext {
    archivesDirPath = "$buildDir/archives"
}

task packageClasses(type: Zip) {
    archiveAppendix = "classes"
    destinationDirectory = file(archivesDirPath)

    from compileJava
}
build.gradle.kts
val archivesDirPath by extra { "$buildDir/archives" }

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

    from(tasks.compileJava)
}

看看我们如何使用compileJava任务作为要打包的文件的源,并在可能会在构建中的其他位置使用的基础上,创建了一个项目属性archivesDirPath来存储放置存档的位置.

直接将任务用作这样的参数依赖于它具有已定义的output ,因此并非总是可能的. 此外,可以通过依赖Java插件的destinationDirectory约定而不是覆盖该约定来进一步改进此示例,但是它的确演示了项目属性的使用.

Single files and directories

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

切勿使用new File(relative path)因为这会创建相对于当前工作目录(CWD)的路径. Gradle无法保证CWD的位置,这意味着依赖它的构建可能随时会损坏.

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

例子21.查找文件
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'))
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"))

As you can see, you can pass strings, File instances and Path instances to the file() method, all of which result in an absolute File object. You can find other options for argument types in the reference guide, linked in the previous paragraph.

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

例子22.创建相对于父项目的路径
build.gradle
File configFile = file("$rootDir/shared/config.xml")
build.gradle.kts
val configFile = file("$rootDir/shared/config.xml")

假设您正在dev/projects/AcmeHealth目录中的多项目构建. 您可以在要修复的库的构建中使用以上示例—位于AcmeHealth/subprojects/AcmePatientRecordLib/build.gradle . 文件路径将解析为dev/projects/AcmeHealth/shared/config.xml的绝对版本.

file()方法可用于配置属性类型为File任何任务. 但是,许多任务可以处理多个文件,因此接下来我们将介绍如何指定文件集.

File collections

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

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

尽管files()方法接受File实例,但是切勿使用new File(relative path) ,因为这会创建相对于当前工作目录(CWD)的路径. Gradle无法保证CWD的位置,这意味着依赖它的构建可能随时会损坏.

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

例子23.创建一个文件集合
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'))
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")
)

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

  • created lazily

  • 遍历

  • filtered

  • combined

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

例子24.实现一个文件集合
build.gradle
task list {
    doLast {
        File srcDir

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

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

        srcDir = file('src2')
        println "Contents of $srcDir.name"
        collection.collect { relativePath(it) }.sort().each { println it }
    }
}
build.gradle.kts
tasks.register("list") {
    doLast {
        var srcDir: File? = null

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

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

        srcDir = file("src2")
        println("Contents of ${srcDir.name}")
        collection.map { relativePath(it) }.sorted().forEach { println(it) }
    }
}
gradle -q list输出
> gradle -q list
Contents of src
src/dir1
src/file1.txt
Contents of src2
src2/dir1
src2/dir2

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

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

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

例子25.使用文件集合
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 + layout.files('src/file2.txt')
        def difference = collection - layout.files('src/file2.txt')
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 + layout.files("src/file2.txt")
        val difference = collection - layout.files("src/file2.txt")

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

例如,在上面的示例中,想象collection在创建union后获得了额外的一两个文件. 只要在将这些文件添加到collection之后使用unionunion还将包含这些其他文件. different文件集合也是如此.

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

例子26.过滤文件集合
build.gradle
        FileCollection textFiles = collection.filter { File f ->
            f.name.endsWith(".txt")
        }
build.gradle.kts
        val textFiles: FileCollection = collection.filter { f: File ->
            f.name.endsWith(".txt")
        }
gradle -q filterTextFiles输出
> gradle -q filterTextFiles
src/file1.txt
src/file2.txt
src/file5.txt

如果collection随时更改(通过添加或从自身删除文件),则textFiles将立即反映该更改,因为它也是一个实时集合. 请注意,传递给filter()的闭包将File作为参数,并且应返回布尔值.

File trees

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

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

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

例子27.创建一个文件树
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*/**')
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*/**")

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

默认情况下, fileTree()返回一个FileTree实例,该实例应用一些默认的排除模式以方便使用-实际上与Ant相同. 有关完整的默认排除列表,请参见Ant手册 .

如果证明这些默认排除有问题,则可以使用defaultexcludes Ant任务解决此问题,如本示例所示:

示例28.更改复制任务的Ant默认排除项
build.gradle
task forcedCopy (type: Copy) {
    into "$buildDir/inPlaceApp"
    from 'src/main/webapp'

    doFirst {
        ant.defaultexcludes remove: "**/.git"
        ant.defaultexcludes remove: "**/.git/**"
        ant.defaultexcludes remove: "**/*~"
    }

    doLast {
        ant.defaultexcludes default: true
    }
}
build.gradle.kts
tasks.register<Copy>("forcedCopy") {
    into("$buildDir/inPlaceApp")
    from("src/main/webapp")

    doFirst {
        ant.withGroovyBuilder {
            "defaultexcludes"("remove" to "**/.git")
            "defaultexcludes"("remove" to "**/.git/**")
            "defaultexcludes"("remove" to "**/*~")
        }
    }

    doLast {
        ant.withGroovyBuilder {
            "defaultexcludes"("default" to true)
        }
    }
}

通常,最好在每次更改默认排除项时都将其重置,因为修改对整个构建都是可见的. 上面的示例在其doLast操作中执行了这样的重置.

您可以使用文件树来执行许多与文件集合相同的事情:

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

例子29.使用文件树
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"
}
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}")
}

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

用户通常需要的另一种特定类型的文件树是归档文件,即ZIP文件,TAR文件等.我们接下来将介绍它们.

Using archives as file trees

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

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

例子30.使用档案作为文件树
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'))
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"))

我们介绍的常见方案中,您可以看到提取存档文件的实际示例.

Understanding implicit conversion to file collections

Gradle中的许多对象都有接受一组输入文件的属性. 例如, JavaCompile任务具有一个source属性,该属性定义了要编译的源文件. 您可以使用api docs中提到的files()方法支持的任何类型来设置此属性的值. 举例来说,这意味着您可以将属性设置为FileString ,collection, FileCollection ,甚至是闭包或`Provider.

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

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

例子31.指定一组文件
build.gradle
task compile(type: 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) }
    }
}
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) }
    })
}

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

例子32.附加一组文件
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() }
}
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() })
}

由于这是一个常规约定,因此建议您在自己的自定义任务中遵循它. 具体来说,如果您打算添加一种方法来配置基于集合的属性,请确保该方法追加而不是替换值.

File copying in depth

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

  • 定义复制类型的任务

  • 指定要复制的文件(可能还有目录)

  • Specify a destination for the copied files

But this apparent simplicity hides a rich API that allows fine-grained control of which files are copied, where they go, and what happens to them as they are copied — renaming of the files and token substitution of file content are both possibilities, for example.

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

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

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

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

  • File -用作文件路径

  • FileCollectionFileTree副本中包含集合中的所有文件

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

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

其他需要考虑的是文件路径指的是哪种类型的东西:

  • 一个文件—该文件被原样复制

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

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

这是一个使用多个from()规范的示例,每个规范都有不同的参数类型. 您可能还会注意到,使用闭包(在Groovy中)或提供程序(在Kotlin中)懒惰地配置了into() -一种也可与from()一起使用的技术:

例子33.指定复制任务源文件和目标目录
build.gradle
task anotherCopyTask (type: 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() }
}
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() })
}

请注意,即使语法相似, into()的惰性配置也不同于子规范 . 注意参数的数量以区分它们.

Filtering files

您已经看到可以直接在Copy任务中过滤文件集合和文件树,但是您还可以通过CopySpec.include(java.lang.String ...)CopySpec.exclude(java .lang.String ...)方法.

这两种方法通常都与Ant样式的包含或排除模式一起使用,如PatternFilterable中所述 . 您还可以通过使用带FileTreeElement的闭包来执行更复杂的逻辑,如果应该包括该文件,则返回true否则返回false . 下面的示例演示了这两种形式,确保仅复制.html和.jsp文件,但其内容中带有单词" DRAFT"的那些.html文件除外:

例子34.选择要复制的文件
build.gradle
task copyTaskWithPatterns (type: Copy) {
    from 'src/main/webapp'
    into "$buildDir/explodedWar"
    include '**/*.html'
    include '**/*.jsp'
    exclude { FileTreeElement details ->
        details.file.name.endsWith('.html') &&
            details.file.text.contains('DRAFT')
    }
}
build.gradle.kts
tasks.register<Copy>("copyTaskWithPatterns") {
    from("src/main/webapp")
    into("$buildDir/explodedWar")
    include("**/*.html")
    include("**/*.jsp")
    exclude { details: FileTreeElement ->
        details.file.name.endsWith(".html") &&
            details.file.readText().contains("DRAFT")
    }
}

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

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

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

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

创建合并的包含和排除规范时请牢记这些规则,以便最终获得所需的确切行为.

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

Renaming files

如何重命名复制文件示例为您提供了执行此操作所需的大多数信息. 它演示了两个重命名选项:

  • 使用正则表达式

  • 使用闭包

正则表达式是一种灵活的重命名方法,尤其是在Gradle支持正则表达式组的情况下,该组允许您删除和替换部分源文件名. 下面的示例说明如何使用简单的正则表达式从包含字符串的任何文件名中删除字符串" -staging-":

例子35.在复制文件时重命名
build.gradle
task rename (type: Copy) {
    from 'src/main/webapp'
    into "$buildDir/explodedWar"
    // Use a closure to convert all file names to upper case
    rename { String fileName ->
        fileName.toUpperCase()
    }
    // Use a regular expression to map the file name
    rename '(.+)-staging-(.+)', '$1$2'
    rename(/(.+)-staging-(.+)/, '$1$2')
}
build.gradle.kts
tasks.register<Copy>("rename") {
    from("src/main/webapp")
    into("$buildDir/explodedWar")
    // Use a closure to convert all file names to upper case
    rename { fileName: String ->
        fileName.toUpperCase()
    }
    // Use a regular expression to map the file name
    rename("(.+)-staging-(.+)", "$1$2")
    rename("(.+)-staging-(.+)".toRegex().pattern, "$1$2")
}

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

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

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

  1. 如果对第一个参数使用斜杠字符串(用'/'分隔),则必须如上面的示例所示,为rename()加上括号.

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

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

rename()的闭包语法非常简单,可以用于简单正则表达式无法处理的任何要求. 系统会为您提供文件名,并为该文件返回一个新名称;如果您不想更改名称,则返回null . 请注意,将对复制的每个文件执行关闭操作,因此请尽量避免执行昂贵的操作.

Filtering file content (token substitution, templating, etc.)

不要与过滤复制哪些文件相混淆, 文件内容过滤使您可以在复制文件时变换其内容. 这可能涉及使用令牌替换的基本模板,删除文本行,或者使用成熟的模板引擎进行更复杂的过滤.

下面的示例演示了几种过滤形式,包括使用CopySpec.expand(java.util.Map)方法进行令牌替换,以及使用带Ant过滤器的 CopySpec.filter(java.lang.Class)进行令牌替换:

例子36.在复制文件时过滤文件
build.gradle
import org.apache.tools.ant.filters.FixCrLfFilter
import org.apache.tools.ant.filters.ReplaceTokens

task filter(type: Copy) {
    from 'src/main/webapp'
    into "$buildDir/explodedWar"
    // Substitute property tokens in files
    expand(copyright: '2009', version: '2.3.1')
    expand(project.properties)
    // 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'
}
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("$buildDir/explodedWar")
    // Substitute property tokens in files
    expand("copyright" to "2009", "version" to "2.3.1")
    expand(project.properties)
    // 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"
}

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

  • 一个使用FilterReader并旨在与Ant过滤器(例如ReplaceTokens

  • 可以使用一个闭包或Transformer ,它为源文件的每一行定义转换

请注意,两个变体都假定源文件是基于文本的. 当您将ReplaceTokens类与filter() ,结果是模板引擎用您定义的值替换@tokenName@形式的标记(Ant样式的标记).

expand()方法将源文件视为Groovy模板 ,该模板评估并扩展${expression}形式的${expression} . 您可以传入属性名称和值,然后在源文件中对其进行扩展. 由于嵌入式表达式是成熟的Groovy表达式,所以expand()可以进行基本的令牌替换.

良好的做法是在读写文件时指定字符集,否则转换对于非ASCII文本将无法正常工作. 您可以使用CopySpec.getFilteringCharset()属性配置字符集. 如果未指定,则使用JVM默认字符集,该字符集可能与所需的字符集不同.

Using the CopySpec class

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

  1. 它们可以独立于任务

  2. 他们是分层的

这些属性中的第一个属性使您可以在构建中共享副本规范 . 第二个在整个复印规范内提供细粒度的控制.

Sharing copy specs

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

Gradle提供的一种解决方案是Project.copySpec(org.gradle.api.Action)方法. 这使您可以在任务外部创建副本规格,然后可以使用CopySpec.with(org.gradle.api.file.CopySpec ......)方法将其附加到适当的任务. 下面的示例演示了如何完成此操作:

例子37.共享副本规范
build.gradle
CopySpec webAssetsSpec = copySpec {
    from 'src/main/webapp'
    include '**/*.html', '**/*.png', '**/*.jpg'
    rename '(.+)-staging(.+)', '$1$2'
}

task copyAssets (type: Copy) {
    into "$buildDir/inPlaceApp"
    with webAssetsSpec
}

task distApp(type: Zip) {
    archiveFileName = 'my-app-dist.zip'
    destinationDirectory = file("$buildDir/dists")

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

tasks.register<Copy>("copyAssets") {
    into("$buildDir/inPlaceApp")
    with(webAssetsSpec)
}

tasks.register<Zip>("distApp") {
    archiveFileName.set("my-app-dist.zip")
    destinationDirectory.set(file("$buildDir/dists"))

    from(appClasses)
    with(webAssetsSpec)
}

copyAssetsdistApp任务都将处理webAssetsSpec指定的src/main/webapp下的静态资源.

通过定义的配置webAssetsSpec不适用于由包括应用类distApp任务. 这是因为from appClasses是其自己的子规范, with webAssetsSpec .

这可能会使您感到困惑,因此最好将with()视为任务中的from()规范的额外内容. 因此,没有定义至少一个from()来定义一个独立的副本规范是没有意义的.

如果遇到要将相同的副本配置应用于不同文件集的情况,则可以直接共享配置块,而无需使用copySpec() . 这是一个具有两个独立任务的示例,这些任务恰好只希望处理图像文件:

例子38.仅共享复制模式
build.gradle
def webAssetPatterns = {
    include '**/*.html', '**/*.png', '**/*.jpg'
}

task copyAppAssets(type: Copy) {
    into "$buildDir/inPlaceApp"
    from 'src/main/webapp', webAssetPatterns
}

task archiveDistAssets(type: Zip) {
    archiveFileName = 'distribution-assets.zip'
    destinationDirectory = file("$buildDir/dists")

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

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

tasks.register<Zip>("archiveDistAssets") {
    archiveFileName.set("distribution-assets.zip")
    destinationDirectory.set(file("$buildDir/dists"))

    from("distResources", webAssetPatterns)
}

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

Using child specifications

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

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

这不是简单的副本,因为项目中不存在WEB-INF目录及其子目录,因此必须在复制期间创建它们. 另外,我们只希望HTML和图像文件直接进入根文件夹( build/explodedWar ,而仅JavaScript文件进入js目录. 因此,我们需要针对这两套文件使用单独的过滤器模式.

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

例子39.嵌套的副本规格
build.gradle
task nestedSpecs(type: Copy) {
    into "$buildDir/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
    }
}
build.gradle.kts
tasks.register<Copy>("nestedSpecs") {
    into("$buildDir/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)
    }
}

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

上面的例子也说明了如何可以通过使用一个孩子将文件复制到目的地的子目录into()from()或孩子from()上的into() 两种方法都是可以接受的,但是您可能需要创建并遵循约定以确保各个构建文件之间的一致性.

不要混淆您的into()规范! 对于普通副本(一个到文件系统而不是一个存档),应该始终有一个 "根" into() ,它仅指定副本的整个目标目录. 其他into()应该附加一个子规范,其路径将相对于根into() .

最后要注意的一点是,子副本规范继承了其目标路径,包括其模式,排除模式,复制操作,名称映射和来自其父级的过滤器. 因此,请注意放置配置的位置.

Copying files in your own tasks

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

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

例子40.使用copy()方法复制文件而不进行最新检查
build.gradle
task copyMethod {
    doLast {
        copy {
            from 'src/main/webapp'
            into "$buildDir/explodedWar"
            include '**/*.html'
            include '**/*.jsp'
        }
    }
}
build.gradle.kts
tasks.register("copyMethod") {
    doLast {
        copy {
            from("src/main/webapp")
            into("$buildDir/explodedWar")
            include("**/*.html")
            include("**/*.jsp")
        }
    }
}

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

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

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

以下示例显示了如何通过使用动态API进行任务输入和输出来解决这些限制:

例子41.使用带有最新检查的copy()方法复制文件
build.gradle
task 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.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")
        }
    }
}

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

Mirroring directories and file collections with the Sync task

The Sync task, which extends the Copy task, copies the source files into the destination directory and then removes any files from the destination directory which it did not copy. In other words, it synchronizes the contents of a directory with its source. This can be useful for doing things such as installing your application, creating an exploded copy of your archives, or maintaining a copy of the project’s dependencies.

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

例子42.使用同步任务复制依赖关系
build.gradle
task libs(type: Sync) {
    from configurations.runtime
    into "$buildDir/libs"
}
build.gradle.kts
tasks.register<Sync>("libs") {
    from(configurations["runtime"])
    into("$buildDir/libs")
}

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

Archive creation in depth

档案本质上是独立的文件系统,Gradle照这样对待它们. 这就是为什么使用存档与使用文件和目录非常相似,包括文件权限之类的原因.

开箱即用,Gradle支持创建ZIP和TAR归档文件,并且通过扩展支持Java的JAR,WAR和EAR格式-Java的归档文件格式均为ZIP. 这些格式中的每一个都有一个相应的任务类型来创建它们: ZipTarJarWarEar . 所有这些都以相同的方式工作,并且基于复制规范,就像" Copy任务一样.

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

例子43.将目录归档为ZIP
build.gradle
task packageDistribution(type: Zip) {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = file("$buildDir/dist")

    from "$buildDir/toArchive"
}
build.gradle.kts
tasks.register<Zip>("packageDistribution") {
    archiveFileName.set("my-distribution.zip")
    destinationDirectory.set(file("$buildDir/dist"))

    from("$buildDir/toArchive")
}

在下一节中,您将学习基于约定的档案名称,这可以避免您始终配置目标目录和档案名称.

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

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

Archive naming

Gradle关于档案的命名以及根据项目使用的插件在何处创建档案有一些约定. 基本约定由基本插件提供 ,该默认插件默认在$buildDir/distributions目录中创建归档文件,并且通常使用格式为[projectName]-[version].[type]的归档文件名称.

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

例子44.创建ZIP档案
build.gradle
plugins {
    id 'base'
}

version = 1.0

task myZip(type: Zip) {
    from 'somedir'

    doLast {
        println archiveFileName.get()
        println relativePath(destinationDirectory)
        println relativePath(archiveFile)
    }
}
build.gradle.kts
plugins {
    base
}

version = "1.0"

tasks.register<Zip>("myZip") {
    from("somedir")

    doLast {
        println(archiveFileName.get())
        println(relativePath(destinationDirectory))
        println(relativePath(archiveFile))
    }
}
gradle -q myZip输出
> gradle -q myZip
zipProject-1.0.zip
build/distributions
build/distributions/zipProject-1.0.zip

请注意,归档文件的名称并非来自创建归档文件的任务的名称.

如果要更改生成的存档文件的名称和位置,则可archiveFileName相应任务的archiveFileNamedestinationDirectory属性提供值. 这些优先于其他适用的约定.

或者,您可以使用AbstractArchiveTask.getArchiveFileName()提供的默认存档名称模式: [archiveBaseName]-[archiveAppendix]-[archiveVersion]-[archiveClassifier].[archiveExtension] . 如果需要,可以分别在任务上设置每个属性. 请注意,基本插件将项目名称的约定用于archiveBaseName ,将项目版本的约定用于archiveVersion ,将存档类型的约定用于archiveExtension . 它不提供其他属性的值.

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

例子45.归档任务的配置-定制归档名称
build.gradle
task myCustomZip(type: Zip) {
    archiveBaseName = 'customName'
    from 'somedir'

    doLast {
        println archiveFileName.get()
    }
}
build.gradle.kts
tasks.register<Zip>("myCustomZip") {
    archiveBaseName.set("customName")
    from("somedir")

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

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

例子46.存档任务的配置-附录和分类器
build.gradle
plugins {
    id 'base'
}

version = 1.0
archivesBaseName = "gradle"

task myZip(type: Zip) {
    from 'somedir'
}

task myOtherZip(type: Zip) {
    archiveAppendix = 'wrapper'
    archiveClassifier = 'src'
    from 'somedir'
}

task echoNames {
    doLast {
        println "Project name: ${project.name}"
        println myZip.archiveFileName.get()
        println myOtherZip.archiveFileName.get()
    }
}
build.gradle.kts
plugins {
    base
}

version = "1.0"
base.archivesBaseName = "gradle"

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

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

tasks.register("echoNames") {
    doLast {
        println("Project name: ${project.name}")
        println(myZip.get().archiveFileName.get())
        println(myOtherZip.get().archiveFileName.get())
    }
}
gradle -q echoNames输出
> gradle -q echoNames
Project name: zipProject
gradle-1.0.zip
gradle-wrapper-1.0-src.zip

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

archiveFileNameProperty<String>, default: archiveBaseName-archiveAppendix-archiveVersion-archiveClassifier.archiveExtension

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

archiveFileProvider<RegularFile>, read-only, default: destinationDirectory/archiveFileName

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

destinationDirectoryDirectoryProperty, default: depends on archive type

将生成的归档文件放入的目标目录. 默认情况下,JAR和WAR进入$buildDir/libs . ZIP和$buildDir/distributions进入$buildDir/distributions .

archiveBaseNameProperty<String>, default: project.name

The base name portion of the archive file name, typically a project name or some other descriptive name for what it contains.

archiveAppendixProperty<String>, default: null

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

archiveVersionProperty<String>, default: project.version

归档文件名称的版本部分,通常以正常项目或产品版本的形式.

archiveClassifierProperty<String>, default: null

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

archiveExtensionProperty<String>, default: depends on archive type and compression type

存档的文件扩展名. 默认情况下,此设置基于存档任务类型和压缩类型(如果要创建TAR). 将是以下之一: zipjarwartartgztbz2 . 当然,您可以根据需要将其设置为自定义扩展名.

Reproducible builds

有时,需要在不同的机器上逐字节地完全相同地创建归档. 您希望确保无论在何时何地从源代码构建工件都可以产生相同的结果. 这对于诸如reproducible-builds.org之类的项目是必需的.

由于一个存档中文件的顺序受基础文件系统的影响,因此复制相同的逐字节存档会带来一些挑战. 每次从源代码构建ZIP,TAR,JAR,WAR或EAR时,归档文件中文件的顺序可能会更改. 时间戳不同的文件也会导致不同版本的存档之间存在差异. Gradle随附的所有AbstractArchiveTask (例如Jar,Zip)任务都支持生成可复制的存档.

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

例子47.激活可复制的档案
build.gradle
tasks.withType(AbstractArchiveTask) {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}
build.gradle.kts
tasks.withType<AbstractArchiveTask>().configureEach {
    isPreserveFileTimestamps = false
    isReproducibleFileOrder = true
}

通常,您将需要发布档案,以便可以从另一个项目中使用它. 旧版发行中介绍了此过程.