文件操作几乎是每个 Gradle 构建的基础。它们涉及处理源文件、管理文件依赖项和生成报告。Gradle 提供了一个强大的 API 来简化这些操作,使开发人员能够轻松执行必要的文件任务。

硬编码路径和惰性

在构建脚本中避免硬编码路径是最佳实践。

除了避免硬编码路径外,Gradle 还鼓励在其构建脚本中使用惰性评估。这意味着 Task 和操作应推迟到实际需要时再执行,而不是立即执行。

本章中的许多示例使用硬编码路径作为字符串字面值。这使得它们易于理解,但这并非好的实践。问题在于路径经常变化,并且需要修改的地方越多,就越有可能遗漏一处并导致构建失败。

在可能的情况下,应按 Task、Task 属性和项目属性的优先顺序来配置文件路径。

例如,如果您创建一个打包 Java 应用程序编译类的 Task,应使用类似于以下的实现方式:

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 Task 是要打包的文件的来源,项目属性 archivesDirPath 存储归档文件的位置,因为我们很可能在构建的其他地方使用它。

像这样直接将 Task 用作参数依赖于其拥有已定义的输出,因此并非总是可行。可以通过依赖 Java 插件对 destinationDirectory 的约定而不是覆盖它来进一步改进此示例,但这确实展示了项目属性的使用。

定位文件

要对文件执行某些操作,您需要知道它在哪里,这正是文件路径提供的信息。Gradle 构建在标准 Java File 类之上,该类表示单个文件的位置,并提供了处理路径集合的 API。

使用 ProjectLayout

ProjectLayout 类用于访问项目中的各种目录和文件。它提供了检索项目目录、构建目录、settings 文件以及项目文件结构中其他重要位置的路径的方法。当您需要在不同项目路径的构建脚本或插件中处理文件时,此类特别有用。

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

您可以在服务中了解有关 ProjectLayout 类的更多信息。

使用 Project.file()

Gradle 提供了 Project.file(java.lang.Object) 方法,用于指定单个文件或目录的位置。

相对路径相对于项目目录进行解析,而绝对路径保持不变。

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

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

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() 方法始终会将相对路径转换为相对于当前项目目录(可能是子项目)的路径。

使用 ProjectLayout.settingsDirectory()

要使用相对于 settings 目录的路径,请访问 Project.layout,从中检索 settingsDirectory,然后构建绝对路径。

例如:

build.gradle.kts
val configFile = layout.settingsDirectory.file("shared/config.xml").asFile
build.gradle
File configFile = layout.settingsDirectory.file("shared/config.xml").asFile

假设您正在目录 dev/projects/AcmeHealth 中处理一个多项目构建。上面的构建脚本位于 AcmeHealth/subprojects/AcmePatientRecordLib/build.gradle。绝对文件路径将解析为 dev/projects/AcmeHealth/shared/config.xml

dev
├── projects
│   ├── AcmeHealth
│   │   ├── subprojects
│   │   │   ├── AcmePatientRecordLib
│   │   │   │   └── build.gradle
│   │   │   └── ...
│   │   ├── shared
│   │   │   └── config.xml
│   │   └── ...
│   └── ...
└── settings.gradle

请注意,对于多项目构建,Project 还提供了 Project.getRootProject(),在示例中,它将解析为 dev/projects/AcmeHealth/subprojects/AcmePatientRecordLib

使用 FileCollection

文件集合(file collection)就是一组文件路径,由 FileCollection 接口表示。

这组路径可以是任何文件路径。这些文件路径之间不需要有任何关系,因此它们不必在同一个目录中或拥有共享的父目录。

指定文件集合的推荐方法是使用 ProjectLayout.files(java.lang.Object...) 方法,该方法返回一个 FileCollection 实例。这个灵活的方法允许您传递多个字符串、File 实例、字符串集合、File 集合等等。如果 Task 具有已定义的输出,您也可以将 Task 作为参数传递。

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
Contents of src
src/dir1
src/file1.txt
Contents of src2
src2/dir1
src2/dir2

惰性创建的关键是向 files() 方法传递一个闭包(Groovy 中)或一个 Provider(Kotlin 中)。您的闭包或 provider 必须返回 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')

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

例如,想象一下上面示例中的 collection 在创建 union 之后增加了一两个文件。只要您在将这些文件添加到 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
src/file1.txt
src/file2.txt
src/file5.txt

如果 collection 随时发生变化,无论是通过添加还是删除文件,textFiles 都会立即反映此变化,因为它也是一个动态集合。请注意,传递给 filter() 的闭包将 File 作为参数,并且应返回布尔值。

理解隐式转换为文件集合

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

这是特定 Task 的特性!这意味着并非所有具有 FileCollectionFileTree 属性的 Task 都会发生隐式转换。如果您想了解在特定情况下是否会发生隐式转换,需要查阅相关文档,例如相应 Task 的 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 Task 中有对应的方法。这些方法遵循追加值集合而不是替换它们的约定。同样,此方法接受 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() }
}

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

使用 FileTree

文件树(file tree)是一个文件集合,它保留了所包含文件的目录结构,并且类型为 FileTree。这意味着文件树中的所有路径都必须有一个共享的父目录。下图突出显示了文件树和文件集合在典型文件复制情况下的区别:

file collection vs file tree
尽管 FileTree 扩展了 FileCollection(是一种继承关系),但它们的行为有所不同。换句话说,您可以在需要文件集合的任何地方使用文件树,但请记住,文件集合是文件的平面列表/集合,而文件树是文件和目录的层次结构。要将文件树转换为平面集合,请使用 FileTree.getFiles() 属性。

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

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

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

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

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

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

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

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

您可以在 PatternFilterable 的 API 文档中查看更多支持的模式示例。

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

如果这些默认排除项造成问题,您可以通过修改 settings 脚本中的默认排除项来解决此问题:

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 不支持在执行阶段更改默认排除项。

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

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

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 中复制文件主要使用 CopySpec,这是一种简化管理项目构建过程中的资源(例如源代码、配置文件和其他资产)的机制。

理解 CopySpec

CopySpec 是一个复制规范,允许您定义要复制哪些文件、从何处复制以及复制到何处。它提供了一种灵活且富有表现力的方式来指定复杂的文件复制操作,包括根据模式过滤文件、重命名文件以及根据各种标准包含/排除文件。

Copy Task 使用 CopySpec 实例来指定要复制的文件和目录。

CopySpec 有两个重要属性:

  1. 它独立于 Task,允许您在构建中共享复制规范

  2. 它是分层的,可在整体复制规范中提供细粒度控制

1. 共享复制规范

考虑一个构建,其中包含多个 Task,用于复制项目的静态网站资源或将其添加到归档文件中。一个 Task 可能将资源复制到本地 HTTP 服务器的文件夹中,另一个 Task 可能将其打包成发行版。每次需要时,您可以手动指定文件位置和相应的包含项,但这更容易引入人为错误,导致 Task 之间不一致。

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

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 这两个 Task 都将处理 src/main/webapp 下的静态资源,如 webAssetsSpec 中所指定。

webAssetsSpec 定义的配置适用于 distApp Task 包含的应用类。这是因为 from appClasses 是其自身的子规范,独立于 with webAssetsSpec

这可能会令人困惑,因此最好将 with() 视为 Task 中一个额外的 from() 规范。因此,在未定义至少一个 from() 的情况下定义一个独立的复制规范是没有意义的。

假设您遇到一个场景,想要将相同的复制配置应用于不同的文件集。在这种情况下,您可以直接共享配置块,而无需使用 copySpec()。这里有一个示例,其中包含两个独立的 Task,它们都恰好只想处理图像文件:

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() 规范。这不仅适用于包含,也适用于排除、文件重命名和文件内容过滤。

2. 使用子规范

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

exploded war child copy spec example

这不是简单的复制,因为项目内不存在 WEB-INF 目录及其子目录,因此必须在复制期间创建它们。此外,我们只想让 HTML 和图像文件直接进入根文件夹 — build/explodedWar — 并只让 JavaScript 文件进入 js 目录。我们需要为这两组文件使用单独的过滤模式。

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

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()

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

使用 Sync Task

Sync Task 继承自 Copy Task,它将源文件复制到目标目录,然后从目标目录中删除所有未复制的文件。它使其源目录的内容与目标目录保持同步。

这对于安装您的应用程序、创建归档文件的展开副本或维护项目依赖项的副本等操作非常有用。

这是一个示例,它在 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) 方法在自己的 Task 中执行相同的功能。

使用 Copy Task

您可以通过创建 Gradle 内置 Copy Task 的实例,并配置文件的位置以及要放置的位置来复制文件。

此示例模拟将生成的报告复制到将打包到归档文件(如 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")
}

然后使用文件和目录路径指定要使用 Copy.from(java.lang.Object…​) 复制的文件,以及要使用 Copy.into(java.lang.Object) 复制到的目录。

尽管硬编码路径使示例变得简单,但它们会使构建变得脆弱。使用可靠的单一事实来源,例如 Task 或共享项目属性,是更好的方法。在以下修改后的示例中,我们使用在其他地方定义的报告 Task,该 Task 的 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 归档,该 Task 为我们提供了将被归档的目录,因此也是我们想要放置报告副本的位置。

复制多个文件

通过向 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 文件:

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

您可以使用 Ant 风格的通配符模式 (**/*) 包含子目录中的文件,如下面更新的示例所示:

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")
}

这个 Task 具有以下效果:

copy with deep filter example

请记住,像这样的深度过滤会产生副作用,即复制 reports 下的目录结构和文件。如果您想复制文件而不带目录结构,则必须使用显式表达式 fileTree(dir) { includes }.files

复制目录层次结构

您可能需要复制文件及其所在的目录结构。当您将目录指定为 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(),而上一节中的指令应用于整个 Task。复制规范中这些不同的粒度级别使您可以轻松处理将遇到的大多数需求。

理解文件复制

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

  • 定义一个类型为 Copy 的 Task

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

  • 指定复制文件的目标位置

但这种看似简单的方式隐藏了一个丰富的 API,该 API 允许精细控制复制哪些文件、复制到哪里以及复制过程中对它们进行什么操作——例如,可以对文件进行重命名和对文件内容进行令牌替换。

让我们从列表中的最后两项开始,它们涉及 CopySpecCopy Task 实现的 CopySpec 接口提供了:

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

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

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

  • 一个 File — 用作文件路径

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

  • 一个任务 — 构成任务已定义输出的文件或目录都将包含在内

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

另外需要考虑的是文件路径指向的是哪种类型的东西

  • 文件 — 文件按原样复制

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

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

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

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

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

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

如这里所述,在执行时使用 Project.copy 方法与配置缓存不兼容。一个可能的解决方案是将任务实现为一个 proper class,并改用 FileSystemOperations.copy 方法,如配置缓存章节所述。

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

解决方案是使用 Project.copy(org.gradle.api.Action) 方法。使用 copy spec 配置它就像 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() 方法的自定义任务应声明与复制操作相关的必要输入和输出。

重命名文件

在 Gradle 中重命名文件可以使用 CopySpec API 来完成,该 API 提供了在复制文件时重命名文件的方法。

使用 Copy.rename()

如果您的构建使用和生成的文件有时没有合适的名称,您可以在复制它们时重命名这些文件。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() 上的子规范的一部分来重命名文件的子集。

使用 Copyspec.rename{}

关于如何在复制时重命名文件的示例提供了执行此操作所需的大部分信息。它演示了重命名的两种选项

  1. 使用正则表达式

  2. 使用闭包

正则表达式是一种灵活的重命名方法,特别是 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。请注意,闭包将针对每个复制的文件执行,因此请尽量避免进行开销大的操作。

过滤文件

在 Gradle 中过滤文件涉及根据特定标准选择性地包含或排除文件。

使用 CopySpec.include()CopySpec.exclude()

您可以通过 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 中过滤文件内容涉及用动态值替换文件中的占位符或标记。

使用 CopySpec.filter()

在复制文件时转换其内容涉及使用标记替换的基本模板,删除文本行,或甚至使用功能齐全的模板引擎进行更复杂的过滤。

以下示例演示了几种过滤形式,包括使用 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 风格的标记)的标记替换为您定义的值。

使用 CopySpec.expand()

expand() 方法将源文件视为 Groovy 模板,后者会评估和展开形式为 ${expression} 的表达式。

您可以传入属性名称和值,这些名称和值随后会在源文件中展开。expand() 不仅支持基本的标记替换,因为嵌入的表达式是完整的 Groovy 表达式。

在读写文件时指定字符集是一个好的做法。否则,转换对于非 ASCII 文本可能无法正常工作。您可以使用 CopySpec.setFilteringCharset(String) 属性配置字符集。如果未指定,则使用 JVM 的默认字符集,这可能与您想要的字符集不同。

设置文件权限

在 Gradle 中设置文件权限涉及为构建过程中创建或修改的文件或目录指定权限。

使用 CopySpec.filePermissions{}

对于任何涉及复制文件的 CopySpec(无论是 Copy 任务本身,还是任何子规范),您都可以通过 CopySpec.filePermissions {} 配置块来显式设置目标文件的权限。

使用 CopySpec.dirPermissions{}

您也可以通过 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)

使用空的文件或目录权限配置块仍然会显式设置它们,只是设置为固定的默认值。这些配置块内的所有内容都相对于默认值。文件和目录的默认权限不同

  • 文件所有者可读写,可读,其他用户可读 (0644, rw-r—​r--)

  • 目录所有者可读写执行,可读执行,其他用户可读执行 (0755, rwxr-xr-x)

移动文件和目录

在 Gradle 中移动文件和目录是一个简单的过程,可以使用多种 API 完成。在构建脚本中实现文件移动逻辑时,考虑文件路径、冲突和任务依赖关系非常重要。

使用 File.renameTo()

File.renameTo() 是 Java 中的一个方法(以及扩展到 Gradle 的 Groovy DSL 中),用于重命名或移动文件或目录。当您在 File 对象上调用 renameTo() 时,您提供另一个表示新名称或位置的 File 对象。如果操作成功,renameTo() 返回 true;否则,返回 false

值得注意的是,renameTo() 存在一些限制和平台特定的行为。

在此示例中,moveFile 任务使用 Copy 任务类型来指定源目录和目标目录。在 doLast 闭包内部,它使用 File.renameTo() 将文件从源目录移动到目标目录。

task moveFile {
    doLast {
        def sourceFile = file('source.txt')
        def destFile = file('destination/new_name.txt')

        if (sourceFile.renameTo(destFile)) {
            println "File moved successfully."
        }
    }
}

使用 Copy 任务

在此示例中,moveFile 任务将文件 source.txt 复制到目标目录并在复制过程中将其重命名为 new_name.txt。这实现了类似于移动文件的效果。

task moveFile(type: Copy) {
    from 'source.txt'
    into 'destination'
    rename { fileName ->
        'new_name.txt'
    }
}

删除文件和目录

在 Gradle 中删除文件和目录涉及将其从文件系统中移除。

使用 Delete 任务

您可以使用 Delete 任务轻松删除文件和目录。您必须以 Project.files(java.lang.Object…​) 方法支持的方式指定要删除的文件和目录。

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

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"
    }
}

使用 Project.delete()

Project.delete(org.gradle.api.Action) 方法可以删除文件和目录。

此方法接受一个或多个参数,表示要删除的文件或目录。

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

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 的角度来看,将文件打包到归档文件中实际上是一种复制,其目标是归档文件而不是文件系统上的目录。创建归档文件与复制非常相似,具有所有相同的功能。

使用 ZipTarJar 任务

最简单的情况是将整个目录的内容归档,本示例通过创建 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 也这样对待它们。这就是为什么处理归档文件类似于处理文件和目录的原因。

开箱即用,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 关于归档文件命名及其创建位置有几个约定,这些约定基于您的项目使用的插件。主要约定由基本插件提供,该插件默认在 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
archive-naming-1.0.zip
build/distributions
build/distributions/archive-naming-1.0.zip

请注意,归档文件的名称*不*是派生自创建它的任务名称。

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

或者,您可以利用 AbstractArchiveTask.getArchiveFileName() 提供的默认归档文件名模式:[archiveBaseName]-[archiveAppendix]-[archiveVersion]-[archiveClassifier].[archiveExtension]。您可以分别在任务上设置这些属性。请注意,基本插件使用项目名称作为 archiveBaseName 的约定,项目版本作为 archiveVersion 的约定,归档类型作为 archiveExtension 的约定。它不为其他属性提供值。

以下示例——与上例来自同一项目——仅配置 archiveBaseName 属性,从而覆盖项目名称的默认值

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

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

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

您还可以通过使用 project 属性 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
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。当然,您也可以根据需要将其设置为自定义扩展名。

将归档文件用作文件树

归档文件是将目录和文件层次结构打包到单个文件中的文件。换句话说,它是文件树的一种特殊情况,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'))

您可以在下面的解压归档文件部分中看到解压归档文件的实际示例。

使用 AbstractArchiveTask 实现可重现构建

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

重现完全相同的逐字节归档文件会带来一些挑战,因为归档文件中文件的顺序受底层文件系统的影响。每次从源代码构建 ZIP、TAR、JAR、WAR 或 EAR 文件时,归档文件内部的文件顺序都可能发生变化。仅具有不同时间戳的文件也会导致构建之间归档文件的差异。

Gradle 附带的所有 AbstractArchiveTask(例如 Jar、Zip)任务都包含对生成可重现归档文件的支持。

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

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

通常您会希望发布一个归档文件,以便可以在另一个项目中使用它。

解压归档文件

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

使用 Project.zipTreeProject.tarTree

两个相关的函数是 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 忽略因重新映射而产生的空目录,详见下方的“注意”说明。

通过这种技术,你无法改变空目录的目标路径。你可以从这个 issue 中了解更多信息。

如果你是一位 Java 开发者,想知道为什么没有 jarTree() 方法,那是因为 zipTree() 对于 JARs、WARs 和 EARs 文件来说已经非常适用。

创建“uber”或“fat”JAR

在 Java 中,应用程序及其依赖通常被打包为独立的 JAR 文件,放在一个分发归档中。这种情况现在依然存在,但另一种常见的方法是将依赖的类和资源直接放入应用程序 JAR 中,创建所谓的 Uber 或 fat JAR。

在 Gradle 中创建“uber”或“fat”JAR 涉及将所有依赖项打包到单个 JAR 文件中,这使得应用程序的分发和运行更加容易。

使用 Shadow 插件

Gradle 没有创建 uber JAR 的完整内置支持,但你可以使用第三方插件,例如 Shadow 插件 (com.gradleup.shadow) 来实现这一目标。这个插件将你的项目类和依赖打包到单个 JAR 文件中。

使用 Project.zipTree()Jar 任务

要将其他 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 任务都通过这种机制确保必要时创建它们所需的任何输出目录。

使用 File.mkdirsFiles.createDirectories

在需要手动创建目录的情况下,你可以在构建脚本或自定义任务实现中使用标准的 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 任务会自动创建给定路径中的所有必要目录。如果目录已存在,它不会执行任何操作。

使用 Project.mkdir

你可以使用 mkdir 方法在 Gradle 中创建目录,该方法在 Project 对象中可用。此方法接受一个 File 对象或一个表示要创建目录路径的 String

tasks.register('createDirs') {
    doLast {
        mkdir 'src/main/resources'
        mkdir file('build/generated')

        // Create multiple dirs
        mkdir files(['src/main/resources', 'src/test/resources'])

        // Check dir existence
        def dir = file('src/main/resources')
        if (!dir.exists()) {
            mkdir dir
        }
    }
}

安装可执行文件

当你构建一个独立的 可执行文件时,你可能希望将此文件安装到你的系统上,以便它位于你的 PATH 中。

使用 Copy 任务

你可以使用 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")
}

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

将单个文件部署到应用服务器通常指将打包好的应用制品(例如 WAR 文件)传输到应用服务器部署目录的过程。

使用 Copy 任务

在使用应用服务器时,你可以使用 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")
}