文件操作几乎是所有 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 类用于访问项目中的各种目录和文件。它提供了检索项目目录、构建目录、设置文件和项目文件结构中其他重要位置的路径的方法。当您需要在构建脚本或插件中使用不同项目路径中的文件时,此类别特别有用。

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) 方法,用于指定单个文件或目录的位置。

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

切勿使用 new File(relative path),除非将其传递给 file()files()from() 或根据 file()files() 定义的其他方法。否则,这将创建一个相对于当前工作目录 (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()

要使用相对于设置目录的路径,请访问 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 实例、列表或 Paths

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 中)。您的闭包或提供者必须返回 files() 接受的类型的值,例如 List<File>StringFileCollection

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

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

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

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

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

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

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

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

例如,假设在创建 union 之后,上述示例中的 collection 增加了一两个额外的文件。只要您在这些文件添加到 collection 之后使用 unionunion 也会包含这些附加文件。different 文件集合也是如此。

动态集合在筛选时也很重要。假设您想使用文件集合的子集。在这种情况下,您可以利用 FileCollection.filter(org.gradle.api.specs.Spec) 方法来确定要“保留”哪些文件。在以下示例中,我们创建了一个新集合,其中只包含源集合中以 .txt 结尾的文件:

build.gradle.kts
val textFiles: FileCollection = collection.filter { f: File ->
    f.name.endsWith(".txt")
}
build.gradle
FileCollection textFiles = collection.filter { File f ->
    f.name.endsWith(".txt")
}
$ gradle -q filterTextFiles
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(is-a 关系),但它们的行为不同。换句话说,您可以在需要文件集合的任何地方使用文件树,但请记住,文件集合是文件的平面列表/集合,而文件树是文件和目录的层次结构。要将文件树转换为平面集合,请使用 FileTree.getFiles() 属性。

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

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

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

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

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

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

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

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

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

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

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

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

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

DirectoryScanner.removeDefaultExclude('**/.git')
DirectoryScanner.removeDefaultExclude('**/.git/**')
Gradle 不支持在执行阶段更改默认排除项。

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

您还可以使用 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 怎么办?为此,请将包含和/或排除模式附加到复制规范。在这里,我们使用字符串模式仅包含 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,它允许对复制哪些文件、它们到哪里以及它们在复制时发生什么进行细粒度控制——例如,文件重命名和文件内容标记替换都是可能的。

让我们从列表中的最后两项开始,它们涉及 CopySpecCopy 任务实现的 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 方法与配置缓存不兼容。一种可能的解决方案是将任务实现为适当的类,并使用 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() 方法的自定义任务应声明与复制操作相关的必要输入和输出。

重命名文件

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.uppercase()
    }
}
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()

这些方法通常与 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)

还可以为特定文件设置权限。请参阅以下示例:

build.gradle.kts
tasks.register<Copy>("specificPermissions") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWarWithScript"))
    eachFile {
        if (name == "script.sh") {
            permissions {
                user {
                    execute = true
                }
            }
        }
    }
}
build.gradle
tasks.register("specificPermissions", Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWarWithScript')
    eachFile {
        if (name == "script.sh") {
            permissions {
                user {
                    execute = true
                }
            }
        }
    }
}
明确设置文件的文件权限不支持 UP-TO-DATE 检查,因此使用此设置的任务将始终运行。

移动文件和目录

在 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

您还可以通过在 base 块中配置 archivesName 属性来覆盖构建中所有归档任务的默认 archiveBaseName 值,如下面的示例所示。该块由 base 插件定义并由 BasePluginExtension 类支持。

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

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

可重现的归档文件

从 Gradle 9 开始,归档文件默认是可重现的。

理想情况下,归档文件应在不同的机器上精确地(字节对字节)重新创建。从源代码构建工件应该产生相同的结果,无论何时何地构建。这对于 reproducible-builds.org 等项目是必需的。

重现相同的字节对字节归档通常会带来一些挑战,因为归档中文件的顺序可能受到底层文件系统的影响。每次从源代码构建 ZIP、TAR、JAR、WAR 或 EAR 时,归档内部文件的顺序可能会改变。磁盘上的文件也可能具有不同的时间戳或权限,具体取决于环境。

从 Gradle 9.0 开始,所有 AbstractArchiveTask(例如 JarZip)任务默认生成可重现的归档文件:

  • 归档中的文件顺序是确定性的

  • 文件具有固定的时间戳(确切值取决于归档类型)

  • 目录具有固定的权限,设置为 0755

  • 文件具有固定的权限,设置为 0644

您可以进一步自定义目录权限文件权限,同时保持归档文件可重现。

保留文件系统定义的方面

如果需要,您可以恢复可重现归档文件的各个方面。

build.gradle.kts
tasks.withType<AbstractArchiveTask>().configureEach {
    // Use file timestamps from the file system
    isPreserveFileTimestamps = true   (1)
    // Make file order based on the file system
    isReproducibleFileOrder = false   (2)
    // Use permissions from the file system
    useFileSystemPermissions()        (3)
}
build.gradle
tasks.withType(AbstractArchiveTask).configureEach {
    // Use file timestamps from the file system
    preserveFileTimestamps = true  (1)
    // Make file order based on the file system
    reproducibleFileOrder = false  (2)
    // Use permissions from the file system
    useFileSystemPermissions()     (3)
}
1 isPreserveFileTimestamps 设置为 true 以使用文件系统中的时间戳值。
2 isReproducibleFileOrder 设置为 false 以保留基于文件系统的文件顺序。
3 调用 useFileSystemPermissions() 以保留文件系统中的权限。

如果文件系统已经成为权限的真实来源(例如,通过版本控制工具),您可以通过配置属性来保留所有归档任务的权限:

gradle.properties
org.gradle.archives.use-file-system-permissions=true

归档文件中文件的权限

Gradle 归档文件中的文件权限设置方式与复制文件时相同,详见设置文件权限。主要区别在于,归档任务默认将归档文件中文件和目录的权限设置为固定值,为了可重现性,文件权限为 0644,目录权限为 0755

我们认识到两种主要情况,用户可能希望更改归档文件中文件和目录的权限:

  1. 文件系统已是权限的真实来源的情况。对于此类配置,请参阅保留文件系统定义的方面部分。

  2. 归档文件中文件和目录的权限应设置为特定值的情况,例如为脚本文件设置可执行位。对于此类配置,请参阅设置文件权限部分。

解压归档

归档实际上是自包含的文件系统,因此解压它们就是将文件从该文件系统复制到本地文件系统——甚至复制到另一个归档中。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

您可以使用 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
        }
    }
}

安装可执行文件

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

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