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

硬编码路径和延迟加载

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

除了避免硬编码路径外,Gradle 还鼓励在其构建脚本中使用延迟加载。这意味着任务和操作应推迟到实际需要时再执行,而不是急切地执行。

本章中的许多示例都使用硬编码路径作为字符串字面量。这使得它们易于理解,但不是好的实践。问题在于路径经常更改,您需要更改它们的位置越多,您就越有可能遗漏一个并破坏构建。

在可能的情况下,您应该使用任务、任务属性和 项目属性(按优先级顺序)来配置文件路径。

例如,如果您创建一个打包 Java 应用程序的已编译类的任务,则应使用类似于以下内容的实现

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

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

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

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

    from compileJava
}

compileJava 任务是打包文件的源,项目属性 archivesDirPath 存储归档文件的位置,因为我们很可能在构建的其他地方使用它。

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

定位文件

要对文件执行某些操作,您需要知道它的位置,这就是文件路径提供的信息。Gradle 构建在标准 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

文件集合 只是由 FileCollection 接口表示的一组文件路径。

路径集可以是任何文件路径。文件路径不必以任何方式相关,因此它们不必在同一目录中或具有共享的父目录。

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

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

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

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

文件集合在 Gradle 中具有重要属性。它们可以是

  • 延迟创建

  • 迭代

  • 过滤

  • 组合

文件集合的延迟创建 在评估构建运行时构成集合的文件时非常有用。在以下示例中,我们查询文件系统以找出特定目录中存在哪些文件,然后将这些文件转换为文件集合

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

例如,假设上述示例中的 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 任务具有一个 source 属性,用于定义要编译的源文件。您可以使用 files() 方法支持的任何类型设置此属性的值,如 API 文档中所述。这意味着您可以将属性设置为 FileString、集合、FileCollection 甚至闭包或 Provider

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

以下是 source 属性可以接受的不同参数类型的一些示例

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

使用 FileTree

文件树 是一个文件集合,它保留了它包含的文件的目录结构,并且具有 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 是一个复制规范,允许您定义要复制的文件、从何处复制它们以及将它们复制到何处。它提供了一种灵活且富有表现力的方式来指定复杂的文件复制操作,包括基于模式过滤文件、重命名文件以及基于各种条件包含/排除文件。

CopySpec 实例在 Copy 任务中用于指定要复制的文件和目录。

CopySpec 具有两个重要属性

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

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

1. 共享复制规范

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

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

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

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

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

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

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

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

    from appClasses
    with webAssetsSpec
}

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

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

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

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

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

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

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

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

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

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

    from 'distResources', webAssetPatterns
}

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

2. 使用子规范

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

exploded war child copy spec example

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

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

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

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

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

不要将您的 into() 规范混淆。对于正常复制,即复制到文件系统而不是归档文件,应该始终有一个“根” into(),它指定复制的总体目标目录。任何其他 into() 都应附加一个子规范,并且其路径将相对于根 into()

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

使用 Sync 任务

Sync 任务(扩展了 Copy 任务)将源文件复制到目标目录,然后从目标目录中删除它未复制的任何文件。它将目录的内容与其源同步。

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

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

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

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

使用 Copy 任务

您可以通过创建 Gradle 内置 Copy 任务的实例并使用文件的位置以及要放置文件的位置来配置它来复制文件。

此示例模仿将生成的报告复制到将打包到归档文件(如 ZIP 或 TAR)中的目录中

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

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

虽然硬编码路径使示例变得简单,但它们使构建变得脆弱。使用可靠的单一事实来源(例如任务或共享项目属性)更好。在以下修改后的示例中,我们使用在其他地方定义的报告任务,该任务的报告位置存储在其 outputFile 属性中

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

我们还假设报告将由 archiveReportsTask 归档,这为我们提供了将归档的目录,因此也是我们想要放置报告副本的位置。

复制多个文件

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

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

现在,两个文件都已复制到归档目录中。

您还可以使用多个 from() 语句来执行相同的操作,如 文件深度复制 部分的第一个示例所示。

但是,如果您想复制目录中的所有 PDF 文件,而无需指定每个文件,该怎么办?为此,请将包含和/或排除模式附加到复制规范。在这里,我们使用字符串模式仅包含 PDF 文件

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 风格的 glob 模式 (**/*) 来包含子目录中的文件,如这个更新后的示例所示

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

此任务具有以下效果

copy with deep filter example

请记住,像这样的深度过滤器具有复制 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(),而上一节中的指令适用于整个任务。复制规范中这些不同的 粒度级别 使您可以轻松处理您将遇到的大多数需求。

理解文件复制

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

  • 定义一个 Copy 类型的任务

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

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

但是,这种表面上的简单性隐藏了一个丰富的 API,它可以对复制哪些文件、它们去哪里以及复制过程中会发生什么进行细粒度的控制——例如,文件重命名和文件内容的令牌替换都是可能的。

让我们从列表中的最后两项开始,它们涉及到 CopySpecCopySpec 接口(Copy 任务实现了该接口)提供了

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 方法与 配置缓存 不兼容。一个可能的解决方案是将任务实现为适当的类,并改用 FileSystemOperations.copy 方法,如 配置缓存章节 中所述。

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

解决方案是使用 Project.copy(org.gradle.api.Action) 方法。使用复制规范配置它就像 Copy 任务一样。这是一个简单的示例

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

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

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

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

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

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

由于 Copy 任务内置了对增量构建和任务依赖关系推断的支持,因此在任何可能的情况下,最好使用 Copy 任务。这就是为什么 copy() 方法旨在供需要复制文件作为其功能一部分的 自定义任务 使用的原因。使用 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)

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

  • 文件所有者的读 & 写,的读,其他的读(0644rw-r—​r--

  • 目录所有者的读、写 & 执行,的读 & 执行,其他的读 & 执行(0755rwxr-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():两者都是必需的。您通常不会看到它们被显式设置,因为大多数项目都应用了 Base Plugin。它为这些属性提供了一些约定俗成的值。

以下示例演示了这一点;您可以在 归档文件命名 部分了解有关约定的更多信息。

每种类型的归档文件都有其自己的任务类型,最常见的类型是 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 对于归档文件的命名以及基于项目使用的插件在何处创建归档文件有几个约定。主要约定由 Base Plugin 提供,该插件默认在 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]。您可以分别在任务上设置这些属性中的每一个。请注意,Base Plugin 使用项目名称作为 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

您还可以通过使用项目属性 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 正是这样对待归档文件的。

您可以使用 Project.zipTree(java.lang.Object)Project.tarTree(java.lang.Object) 方法来包装相应类型的归档文件(请注意,JAR、WAR 和 EAR 文件都是 ZIP 文件),而不是使用仅适用于普通文件系统的 fileTree() 方法。这两种方法都返回 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 忽略由重新映射产生的空目录,请参阅下面的“注意”

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

如果您是 Java 开发人员,想知道为什么没有 jarTree() 方法,那是因为 zipTree() 对于 JAR、WAR 和 EAR 文件非常适用。

创建 "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

你可以使用 Gradle 中的 mkdir 方法创建目录,该方法在 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
        }
    }
}

安装可执行文件

当你构建一个独立的可执行文件时,你可能希望将此文件安装到你的系统上,以便它最终出现在你的路径中。

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