欢迎访问shiker.tech

请允许在我们的网站上展示广告

您似乎使用了广告拦截器,请关闭广告拦截器。我们的网站依靠广告获取资金。

【译文】Gradle-文件操作与日志设置
(last modified Jun 26, 2023, 12:31 PM )
by
侧边栏壁纸
  • 累计撰写 186 篇文章
  • 累计创建 65 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

【译文】Gradle-文件操作与日志设置

橙序员
2022-09-05 / 0 评论 / 0 点赞 / 1,228 阅读 / 17,093 字 / 正在检测百度是否收录... 正在检测必应是否收录...
文章摘要(AI生成)

文件操作几乎每个 Gradle 构建都以某种方式与文件交互:想想源文件、文件依赖项、报告等等。这就是为什么 Gradle 提供了一个全面的 API,可以让您轻松执行所需的文件操作。API 有两个部分:指定要处理的文件和目录指定如何处理它们深度文件路径部分详细介绍了其中的第一个,而后续部分(如深度文件

文件操作

几乎每个 Gradle 构建都以某种方式与文件交互:想想源文件、文件依赖项、报告等等。这就是为什么 Gradle 提供了一个全面的 API,可以让您轻松执行所需的文件操作。

API 有两个部分:

  • 指定要处理的文件和目录
  • 指定如何处理它们

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

复制单个文件

您可以通过创建 Gradle 的内置Copy任务的实例并将其配置为文件的位置和您想要放置的位置来复制文件。此示例模拟将生成的报告复制到将打包到压缩包中的目录中,例如 ZIP 或 TAR:

示例 1. 如何复制单个文件

build.gradle

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

Project.file(java.lang.Object)方法用于创建相对于当前项目的文件或目录路径,并且是使构建脚本无论项目路径如何都能正常工作的常用方法。然后使用文件和目录路径来指定使用Copy.from(java.lang.Object…)复制哪个文件以及使用 Copy.into( java.lang.Object )将其复制到哪个目录。

您甚至可以不使用该file()方法直接使用路径,如深入文件复制部分中所述:

示例 2. 使用隐式字符串路径

build.gradle

tasks.register('copyReport2', Copy) {
    from "$buildDir/reports/my-report.pdf"
    into "$buildDir/toArchive"
}

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

示例 3. 优先选择任务/项目属性而不是硬编码路径

build.gradle

tasks.register('copyReport3', Copy) {
    from myReportTask.outputFile
    into archiveReportsTask.dirToArchive
}

我们还假设报告将由 压缩包archiveReportsTask,这为我们提供了将被压缩包的目录,因此我们想要放置报告的副本。

复制多个文件

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

示例 4. 在 from() 中使用多个参数

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:

示例 5. 使用平面滤波器

build.gradle

tasks.register('copyPdfReportsForArchiving', Copy) {
    from layout.buildDirectory.dir("reports")
    include "*.pdf"
    into layout.buildDirectory.dir("toArchive")
}

有一点需要注意,如下图所示,只有直接位于reports目录中的 PDF 才会被复制:

使用平面过滤器复制示例

图 1. 平面过滤器对复制的影响

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

示例 6. 使用深层过滤器

build.gradle

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

该任务具有以下效果:

使用深度过滤器复制示例

图 2. 深度过滤器对复制的影响

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

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

复制目录层次结构

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

示例 7. 复制整个目录

build.gradle

tasks.register('copyReportsDirForArchiving', Copy) {
    from layout.buildDirectory.dir("reports")
    into layout.buildDirectory.dir("toArchive")
}

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

那么你如何确保它reports本身被复制,而不是任何其他目录$buildDir?答案是将其添加为包含模式:

示例 8. 复制整个目录,包括它自己

build.gradle

tasks.register('copyReportsDirForArchiving2', Copy) {
    from(layout.buildDirectory) {
        include "reports/**"
    }
    into layout.buildDirectory.dir("toArchive")
}

您将获得与以前相同的行为,除了目标中的目录多了一层,即toArchive/reports.

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

创建压缩包(zip、tar 等)

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

最简单的情况涉及归档目录的全部内容,本示例通过创建toArchive目录的 ZIP 来演示:

示例 9. 将目录归档为 ZIP

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目录在源位置中不存在,因此您必须将其创建为压缩包的一部分。为此,您into()只需为 PDF 添加声明:

示例 10. 使用 Base Plugin 的压缩包名称约定

build.gradle

plugins {
    id 'base'
}

version = "1.0.0"

tasks.register('packageDistribution', Zip) {
    from(layout.buildDirectory.dir("toArchive")) {
        exclude "**/*.pdf"
    }

    from(layout.buildDirectory.dir("toArchive")) {
        include "**/*.pdf"
        into "docs"
    }
}

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

解压文件夹

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

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

示例 11. 解压缩 ZIP 文件

build.gradle

tasks.register('unpackFiles', Copy) {
    from zipTree("src/resources/thirdPartyResources.zip")
    into layout.buildDirectory.dir("resources")
}

与普通副本一样,您可以通过过滤器控制哪些文件被解压缩,甚至可以在文件解压缩时重命名文件

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

示例 12. 解压缩 ZIP 文件的子集

build.gradle

tasks.register('unpackLibsDirectory', Copy) {
    from(zipTree("src/resources/thirdPartyResources.zip")) {
        include "libs/**"  
        eachFile { fcd ->
            fcd.relativePath = new RelativePath(true, fcd.relativePath.segments.drop(1))  
        }
        includeEmptyDirs = false  
    }
    into layout.buildDirectory.dir("resources")
}

仅提取libs目录中的文件子集

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

忽略重新映射产生的空目录,请参阅下面的注意事项

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

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

创建“uber”或“fat”JAR

在 Java 空间中,应用程序及其依赖项通常被打包为单个分发压缩包中的单独 JAR。这仍然会发生,但现在有另一种常见的方法:将依赖项的类和资源直接放入应用程序 JAR,创建所谓的 uber 或 fat JAR。

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

示例 13. 创建 Java uber 或 fat JAR

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

创建目录

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

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

示例 14. 手动创建目录

build.gradle

tasks.register('ensureDirectory') {
    doLast {
        mkdir "images"
    }
}

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

移动文件和目录

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

示例 15. 使用 Ant 任务移动目录

Groovy``Kotlin

构建.gradle

tasks.register('moveReports') {
    doLast {
        ant.move file: "${buildDir}/reports",
                 todir: "${buildDir}/toArchive"
    }
}

这不是一个常见的要求,应该谨慎使用,因为您会丢失信息并且很容易破坏构建。通常最好改为复制目录和文件。

复制时重命名文件

您的构建使用和生成的文件有时没有适合的名称,在这种情况下,您希望在复制这些文件时重命名它们。Gradle 允许您使用配置将其作为复制规范的一部分来执行rename()

以下示例从具有它的任何文件的名称中删除“-staging-”标记:

示例 16. 在复制文件时重命名文件

build.gradle

tasks.register('copyFromStaging', Copy) {
    from "src/main/webapp"
    into layout.buildDirectory.dir('explodedWar')

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

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

示例 17. 在复制文件名时截断文件名

build.gradle

tasks.register('copyWithTruncate', Copy) {
    from layout.buildDirectory.dir("reports")
    rename { String filename ->
        if (filename.size() > 10) {
            return filename[0..7] + "~" + filename.size()
        }
        else return filename
    }
    into layout.buildDirectory.dir("toArchive")
}

与过滤一样,您还可以通过将文件子集配置为from().

删除文件和目录

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

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

示例 18. 删除目录

build.gradle

tasks.register('myClean', Delete) {
    delete buildDir
}

如果您想更好地控制删除哪些文件,则不能以与复制文件相同的方式使用包含和排除。相反,您必须使用 和 的内置过滤FileCollection机制FileTree。下面的示例就是从源目录中清除临时文件:

示例 19. 删除与特定模式匹配的文件

build.gradle

tasks.register('cleanTempFiles', Delete) {
    delete fileTree("src").matching {
        include "**/*.tmp"
    }
}

您将在下一节中了解有关文件集合和文件树的更多信息。

深入的文件路径

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

但首先,关于在构建中使用硬编码文件路径的重要说明。

在硬编码文件路径上

本章中的许多示例都使用硬编码路径作为字符串文字。这使它们易于理解,但对于实际构建来说这不是一个好习惯。问题是路径经常改变,你需要改变的地方越多,你就越有可能错过一个并破坏构建。

在可能的情况下,您应该按照优先顺序使用任务、任务属性和项目属性来配置文件路径。例如,如果您要创建一个打包 Java 应用程序的已编译类的任务,您的目标应该是这样的:

示例 20.如何最小化构建中硬编码路径的数量

build.gradle

def archivesDirPath = layout.buildDirectory.dir('archives')

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

    from compileJava
}

看看我们如何使用compileJava任务作为要打包的文件的源,我们已经创建了一个项目属性archivesDirPath来存储我们放置文件夹的位置,因为我们可能会在构建的其他地方使用它。

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

单个文件和目录

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

永远不要使用new File(relative path),因为这会创建一个相对于当前工作目录 (CWD) 的路径。Gradle 不能保证 CWD 的位置,这意味着依赖它的构建可能随时中断。

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

示例 21. 定位文件

build.gradle

// Using a relative path
File configFile = file('src/config.xml')

// Using an absolute path
configFile = file(configFile.absolutePath)

// Using a File object with a relative path
configFile = file(new File('src/config.xml'))

// Using a java.nio.file.Path object with a relative path
configFile = file(Paths.get('src', 'config.xml'))

// Using an absolute java.nio.file.Path object
configFile = file(Paths.get(System.getProperty('user.home')).resolve('global-config.xml'))

如您所见,您可以将字符串、File实例和Path实例传递给file()方法,所有这些都会产生一个绝对File对象。您可以在参考指南中找到参数类型的其他选项,在上一段中链接。

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

示例 22. 创建相对于父项目的路径

build.gradle

File configFile = file("$rootDir/shared/config.xml")

假设您正在dev/projects/AcmeHealth目录中构建多项目。您在要修复的库的构建中使用上面的示例 - 在AcmeHealth/subprojects/AcmePatientRecordLib/build.gradle. 文件路径将解析为dev/projects/AcmeHealth/shared/config.xml.

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

文件集

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

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

尽管该files()方法接受File实例,但永远不要使用new File(relative path)它,因为这会创建一个相对于当前工作目录 (CWD) 的路径。Gradle 不能保证 CWD 的位置,这意味着依赖它的构建可能随时中断。

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

示例 23. 创建文件集合

build.gradle

FileCollection collection = layout.files('src/file1.txt',
                                  new File('src/file2.txt'),
                                  ['src/file3.csv', 'src/file4.csv'],
                                  Paths.get('src', 'file5.txt'))

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

  • 懒惰地创造
  • 迭代
  • 过滤
  • 结合

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

示例 24. 实现文件集合

build.gradle

tasks.register('list') {
    doLast {
        File srcDir

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

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

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

**gradle -q list**的输出

> gradle -q list
Contents of src
src/dir1
src/file1.txt
Contents of src2
src2/dir1
src2/dir2

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

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

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

示例 25. 使用文件集合

build.gradle

        // Iterate over the files in the collection
        collection.each { File file ->
            println file.name
        }

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

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

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

例如,假设collection在上面的示例中,在创建后获得了一个或两个额外的文件union。只要你union在那些文件被添加到之后使用collectionunion也会包含那些额外的文件。different文件收集也是如此。

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

示例 26.过滤文件集合

build.gradle

        FileCollection textFiles = collection.filter { File f ->
            f.name.endsWith(".txt")
        }

**gradle -q filterTextFiles**的输出

> gradle -q filterTextFiles
src/file1.txt
src/file2.txt
src/file5.txt

如果collection在任何时候发生更改,无论是通过添加或删除自身文件,那么textFiles将立即反映更改,因为它也是一个实时集合。请注意,您传递给的闭包filter()将 aFile作为参数并应返回布尔值。

文件树

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

文件集合与文件树

图 3. 复制文件时文件树和文件集合行为方式的差异

尽管FileTree扩展FileCollection(is-a 关系),但它们的行为确实不同。换句话说,您可以在需要文件集合的任何地方使用文件树,但请记住:文件集合是文件的平面列表/集合,而文件树是文件和目录层次结构。要将文件树转换为平面集合,请使用FileTree.getFiles()属性。

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

示例 27. 创建文件树

build.gradle

// Create a file tree with a base directory
ConfigurableFileTree tree = fileTree(dir: 'src/main')

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

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

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

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

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

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

示例 28. 在设置脚本中更改默认排除项

settings.gradle

import org.apache.tools.ant.DirectoryScanner

DirectoryScanner.removeDefaultExclude('**/.git')
DirectoryScanner.removeDefaultExclude('**/.git/**')

目前,Gradle 的默认排除是通过 Ant 的DirectoryScanner类配置的。Gradle 不支持在执行阶段更改默认排除项。

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

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

示例 29. 使用文件树

build.gradle

// Iterate over the contents of a tree
tree.each {File file ->
    println file
}

// Filter a tree
FileTree filtered = tree.matching {
    include 'org/gradle/api/**'
}

// Add trees together
FileTree sum = tree + fileTree(dir: 'src/test')

// Visit the elements of the tree
tree.visit {element ->
    println "$element.relativePath => $element.file"
}

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

用户通常需要的另一种特定类型的文件树是压缩包,即 ZIP 文件、TAR 文件等。我们接下来看看这些文件。

使用文件夹作为文件树

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

以下是创建基于压缩包的文件树的一些简单示例:

示例 30. 使用压缩包作为文件树

build.gradle

// Create a ZIP file tree using path
FileTree zip = zipTree('someFile.zip')

// Create a TAR file tree using path
FileTree tar = tarTree('someFile.tar')

//tar tree attempts to guess the compression based on the file extension
//however if you must specify the compression explicitly you can:
FileTree someTar = tarTree(resources.gzip('someTar.ext'))

您可以在我们介绍的常见场景中看到一个提取压缩包文件的实际示例。

了解到文件集合的隐式转换

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

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

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

示例 31. 指定一组文件

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()方法支持的任何类型,如下所示:

示例 32. 附加一组文件

build.gradle

compile {
    // Add some source directories use String paths
    source 'src/main/java', 'src/main/groovy'

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

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

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

文件深度复制

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

  • 定义复制类型的任务
  • 指定要复制的文件(以及可能的目录)
  • 指定复制文件的目的地

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

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

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

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

  • A String— 视为文件路径,或者,如果它以“file://”开头,则视为文件 URI
  • A File— 用作文件路径
  • A FileCollectionor FileTree— 集合中的所有文件都包含在副本中
  • 任务——包括构成任务定义输出的文件或目录

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

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

  • 文件 — 文件按原样复制
  • 一个目录——这实际上被视为一棵文件树:其中的所有内容,包括子目录,都被复制。但是,目录本身不包含在副本中。
  • 一个不存在的文件——路径被忽略

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

示例 33. 指定复制任务源文件和目标目录

build.gradle

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

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

过滤文件

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

这两种方法通常与 Ant 样式的包含或排除模式一起使用,如PatternFilterable中所述。您还可以通过使用一个闭包来执行更复杂的逻辑,该闭包接受一个FileTreeElement并返回true是否应该包含文件或false以其他方式。以下示例演示了这两种形式,确保仅复制 .html 和 .jsp 文件,但内容中包含单词“DRAFT”的 .html 文件除外:

示例 34. 选择要复制的文件

build.gradle

tasks.register('copyTaskWithPatterns', Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWar')
    include '**/*.html'
    include '**/*.jsp'
    exclude { FileTreeElement details ->
        details.file.name.endsWith('.html') &&
            details.file.text.contains('DRAFT')
    }
}

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

  • 如果没有明确的包含或排除,则包含所有内容
  • 如果指定了至少一个包含,则仅包含与模式匹配的文件和目录
  • 任何排除模式都会覆盖任何包含,因此如果文件或目录与至少一个排除模式匹配,则无论包含模式如何,它都不会被包含在内

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

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

重命名文件

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

  • 使用正则表达式
  • 使用闭包

正则表达式是一种灵活的重命名方法,尤其是 Gradle 支持正则表达式组,允许您删除和替换源文件名的一部分。以下示例显示了如何使用简单的正则表达式从包含它的任何文件名中删除字符串“-staging-”:

示例 35. 在复制文件时重命名文件

build.gradle

tasks.register('rename', Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWar')
    // Use a closure to convert all file names to upper case
    rename { String fileName ->
        fileName.toUpperCase()
    }
    // Use a regular expression to map the file name
    rename '(.+)-staging-(.+)', '$1$2'
    rename(/(.+)-staging-(.+)/, '$1$2')
}

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

Groovy 构建脚本中的正则表达式人们在这种情况下使用正则表达式时会遇到两个常见问题:如果您对第一个参数使用斜线字符串(由“/”分隔的字符串),则必须包含括号,rename()如上例所示。第二个参数使用单引号是最安全的,否则你需要在组替换中转义’',即`"\$1\$2"`第一个是一个小不便,但斜线字符串的优点是您不必在正则表达式中转义反斜杠 ('\') 字符。第二个问题源于 Groovy 对使用`{ }`双引号和斜线字符串中的语法的嵌入式表达式的支持。

for 的闭包语法rename()很简单,可用于简单正则表达式无法处理的任何需求。您获得了一个文件的名称,并且您为该文件返回一个新名称,或者null如果您不想更改名称。请注意,将为每个复制的文件执行闭包,因此请尽可能避免昂贵的操作。

过滤文件内容(令牌替换、模板等)

不要与过滤哪些文件被复制混淆,文件内容过滤允许您在复制文件时转换文件的内容。这可能涉及使用标记替换的基本模板、删除文本行,或者使用成熟的模板引擎进行更复杂的过滤。

以下示例演示了几种过滤形式,包括使用CopySpec.expand(java.util.Map)方法的令牌替换和另一种使用带有Ant 过滤器的CopySpec.filter(java.lang.Class)的标记替换:

示例 36. 在复制文件时过滤文件

build.gradle

import org.apache.tools.ant.filters.FixCrLfFilter
import org.apache.tools.ant.filters.ReplaceTokens

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')
    expand(project.properties)
    // Use some of the filters provided by Ant
    filter(FixCrLfFilter)
    filter(ReplaceTokens, tokens: [copyright: '2009', version: '2.3.1'])
    // Use a closure to filter each line
    filter { String line ->
        "[$line]"
    }
    // Use a closure to remove lines
    filter { String line ->
        line.startsWith('-') ? null : line
    }
    filteringCharset = 'UTF-8'
}

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

  • 一个需要 aFilterReader并且被设计为与 Ant 过滤器一起使用,例如ReplaceTokens
  • 一个为源文件的每一行定义转换的闭包或转换器

请注意,这两种变体都假定源文件是基于文本的。当您将ReplaceTokens类与 一起使用时filter(),结果是一个模板引擎,它将表单@tokenName@的标记(Ant 样式标记)替换为您定义的值。

expand()方法将源文件视为Groovy 模板,这些模板评估和扩展表单的表达式${expression}。您可以传入属性名称和值,然后在源文件中展开。expand()由于嵌入式表达式是成熟的 Groovy 表达式,因此不仅可以进行基本的标记替换。

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

使用CopySpec

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

  1. 它们可以独立于任务
  2. 它们是分层的

这些属性中的第一个允许您在构建中共享副本规范。第二个在整个复制规范中提供细粒度的控制。

共享副本规范

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

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

示例 37. 共享副本规范

build.gradle

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

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(). 这是一个示例,它有两个独立的任务,恰好只想处理图像文件:

示例 38. 仅共享复制模式

build.gradle

def webAssetPatterns = {
    include '**/*.html', '**/*.png', '**/*.jpg'
}

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

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

    from 'distResources', webAssetPatterns
}

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

使用子规范

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

爆炸战争子副本规范示例

图 4. 为 Servlet 容器创建分解的 WAR

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

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

示例 39. 嵌套复制规范

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配置如何具有嵌套的包含规范:这是子副本规范。您当然可以根据需要在此处添加内容过滤和重命名。子副本规范仍然是副本规范。

into()上面的示例还演示了如何通过使用 a上的子目录from()或. from()into()这两种方法都可以接受,但您可能希望创建并遵循约定以确保构建文件之间的一致性。

不要into()混淆您的规格!对于一个普通的副本——一个文件系统而不是文件夹——应该总是有一个“根”into()来简单地指定副本的整个目标目录。任何其他into()都应该附加一个子规范,并且它的路径将相对于 root into()

最后要注意的一件事是子副本规范从其父级继承其目标路径、包含模式、排除模式、复制操作、名称映射和过滤器。所以要小心你放置配置的位置。

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

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

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

示例 40. 使用 copy() 方法复制文件而不进行最新检查

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来解决这些限制:

示例 41. 使用带有最新检查的 copy() 方法复制文件

build.gradle

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

这些限制使得Copy尽可能使用任务更可取,因为它内置了对增量构建和任务依赖推断的支持。这就是为什么该copy()方法旨在供需要复制文件作为其功能的一部分的自定义任务使用。使用该copy()方法的自定义任务应声明与复制操作相关的必要输入和输出。

Sync使用任务镜像目录和文件集合

扩展任务的同步Copy任务将源文件复制到目标目录,然后从目标目录中删除它没有复制的所有文件。换句话说,它将目录的内容与其源同步。这对于安装应用程序、创建文件夹的分解副本或维护项目依赖项的副本等操作很有用。

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

示例 42. 使用 Sync 任务复制依赖项

build.gradle

tasks.register('libs', Sync) {
    from configurations.runtime
    into layout.buildDirectory.dir('libs')
}

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

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

使用应用程序服务器时,您可以使用Copy任务来部署应用程序压缩包(例如 WAR 文件)。由于您正在部署单个文件,因此目标目录Copy是整个部署目录。部署目录有时确实包含不可读的文件,例如命名管道,因此 Gradle 在进行最新检查时可能会遇到问题。为了支持这个用例,您可以使用Task.doNotTrackState()

示例 43. 使用 Copy 部署 WAR 文件

build.gradle

plugins {
    id 'war'
}

tasks.register("deployToTomcat", Copy) {
    from war
    into layout.projectDirectory.dir('tomcat/webapps')
    doNotTrackState("Deployment directory contains unreadable files")
}

安装可执行文件

当你构建一个独立的可执行文件时,你可能想在你的系统上安装这个文件,所以它最终会出现在你的路径中。您可以使用Copy任务将可执行文件安装到共享目录中,例如/usr/local/bin. 安装目录可能包含许多其他可执行文件,其中一些甚至可能是 Gradle 无法读取的。要支持Copy任务目标目录中的不可读文件并避免耗时的最新检查,您可以使用Task.doNotTrackState()

示例 44. 使用 Copy 安装可执行文件

build.gradle

tasks.register("installExecutable", Copy) {
    from "build/my-binary"
    into "/usr/local/bin"
    doNotTrackState("Installation directory contains unrelated files")
}

深度归档创建

文件夹本质上是自包含的文件系统,Gradle 就是这样对待它们的。这就是为什么使用文件夹与使用文件和目录非常相似,包括文件权限等。

开箱即用,Gradle 支持创建 ZIP 和 TAR 压缩包,并通过扩展 Java 的 JAR、WAR 和 EAR 格式——Java 的压缩包格式都是 ZIP。这些格式中的每一种都有相应的任务类型来创建它们:ZipTarJarWarEar。这些都以相同的方式工作,并且基于副本规范,就像Copy任务一样。

创建压缩包文件本质上是一个文件副本,其中的目标是隐含的,即压缩包文件本身。这是一个指定目标压缩包文件的路径和名称的基本示例:

示例 45. 将目录归档为 ZIP

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提供,它默认在$buildDir/distributions目录中创建文件夹,通常使用*[projectName]-[version].[type]*形式的文件夹名称。

以下示例来自名为 的项目archive-naming,因此该myZip任务创建了一个名为 的压缩包archive-naming-1.0.zip

示例 46. 创建 ZIP 压缩包

build.gradle

plugins {
    id 'base'
}

version = 1.0

tasks.register('myZip', Zip) {
    from 'somedir'

    doLast {
        println archiveFileName.get()
        println relativePath(destinationDirectory)
        println relativePath(archiveFile)
    }
}

**gradle -q myZip**的输出

> gradle -q myZip
archive-naming-1.0.zip
build/distributions
build/distributions/archive-naming-1.0.zip

请注意,压缩包的名称并非源自创建它的任务的名称。

如果要更改生成的压缩包文件的名称和位置,可以为相应任务的archiveFileName和属性提供值。destinationDirectory这些会覆盖原本适用的任何约定。

或者,您可以使用AbstractArchiveTask.getArchiveFileName()提供的默认压缩包名称模式:[archiveBaseName]-[archiveAppendix]-[archiveVersion]-[archiveClassifier].[archiveExtension]。如果您愿意,您可以在任务上分别设置这些属性中的每一个。请注意,Base Plugin 使用 archiveBaseName 的项目名称、archiveVersion的项目版本和archiveExtension的压缩包类型的约定。它不提供其他属性的值。

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

示例 47. 归档任务的配置 - 自定义归档名称

build.gradle

tasks.register('myCustomZip', Zip) {
    archiveBaseName = 'customName'
    from 'somedir'

    doLast {
        println archiveFileName.get()
    }
}

**gradle -q myCustomZip**的输出

> gradle -q myCustomZip
customName-1.0.zip

您还可以使用项目属性覆盖构建中所有压缩包任务的默认archiveBaseName值,如以下示例所示:archivesBaseName

示例 48. 归档任务的配置 - 附录和分类器

build.gradle

plugins {
    id 'base'
}

version = 1.0
// tag::base-plugin-config[]
base {
    archivesName = "gradle"
    distsDirectory = layout.buildDirectory.dir('custom-dist')
    libsDirectory = layout.buildDirectory.dir('custom-libs')
}
// end::base-plugin-config[]

tasks.register('myZip', Zip) {
    from 'somedir'
}

tasks.register('myOtherZip', Zip) {
    archiveAppendix = 'wrapper'
    archiveClassifier = 'src'
    from 'somedir'
}

tasks.register('echoNames') {
    doLast {
        println "Project name: ${project.name}"
        println myZip.archiveFileName.get()
        println myOtherZip.archiveFileName.get()
    }
}

**gradle -q echoNames**的输出

> gradle -q echoNames
Project name: archives-changed-base-name
gradle-1.0.zip
gradle-wrapper-1.0-src.zip

您可以在AbstractArchiveTask的 API 文档中找到所有可能的压缩包任务属性,但我们也在此处总结了主要属性:

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

    生成的压缩包的完整文件名。如果默认值中的任何属性为空,则删除它们的“-”分隔符。

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

    生成压缩包的绝对文件路径。

  • destinationDirectoryDirectoryProperty,默认值:取决于压缩包类型

    放置生成压缩包的目标目录。默认情况下,JAR 和 WAR 进入$buildDir/libs. ZIP 和 TAR 进入$buildDir/distributions.

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

    压缩包文件名的基本名称部分,通常是项目名称或其他描述性名称。

  • archiveAppendixProperty<String>,默认值:null

    紧跟在基本名称之后的压缩包文件名的附录部分。它通常用于区分不同形式的内容,例如代码和文档,或最小分布与完整或完整的分布。

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

    压缩包文件名的版本部分,通常采用普通项目或产品版本的形式。

  • archiveClassifierProperty<String>,默认值:null

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

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

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

在多个文件夹之间共享内容

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

可重现的构建

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

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

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

示例 49. 激活可重现的压缩包

build.gradle

tasks.withType(AbstractArchiveTask).configureEach {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}

通常你会想要发布一个压缩包,以便它可以从另一个项目中使用。这个过程在跨项目出版物中有所描述。

日志设置

日志是构建工具的主要“UI”。如果它过于冗长,真正的警告和问题很容易被它隐藏起来。另一方面,您需要相关信息来确定是否出现问题。Gradle 定义了 6 个日志级别,如日志级别所示。除了您通常会看到的日志级别之外,还有两个特定于 Gradle 的日志级别。这些级别是QUIETLIFECYCLE。后者是默认设置,用于报告构建进度。

日志级别

ERROR 错误信息
QUIET 重要信息消息
WARNING 警告信息
LIFECYCLE 进度信息消息
INFO 信息消息
DEBUG 调试消息

无论使用何种日志级别,都会显示控制台的丰富组件(构建状态和正在进行的工作区域)。在 Gradle 4.0 之前,那些丰富的组件只显示在日志级别LIFECYCLE或更低级别。

选择日志级别

您可以使用日志级别命令行选项中显示的命令行开关来选择不同的日志级别。您还可以使用配置日志级别gradle.properties,请参阅Gradle 属性。在Stacktrace 命令行选项中,您可以找到影响 stacktrace 日志记录的命令行开关。

选项 输出日志级别
没有日志记录选项 生命周期及更高
-q或者--quiet 安静及更高
-w或者--warn WARN 及更高版本
-i或者--info INFO 及以上
-d或者--debug DEBUG 及更高版本(即所有日志消息)

DEBUG日志级别可以将安全敏感信息暴露给控制台

Stacktrace 命令行选项

  • -s或者--stacktrace

    打印截断的堆栈跟踪。我们建议在完整的堆栈跟踪中这样做。Groovy 完整的堆栈跟踪非常冗长(由于底层的动态调用机制。但它们通常不包含代码中出现问题的相关信息*。*)此选项呈现堆栈跟踪以用于弃用警告。

  • -S或者--full-stacktrace

    完整的堆栈跟踪被打印出来。此选项为弃用警告呈现堆栈跟踪。

  • <无堆栈跟踪选项>

    在构建错误(例如编译错误)的情况下,不会将堆栈跟踪打印到控制台。只有在内部异常的情况下才会打印堆栈跟踪。如果DEBUG选择了日志级别,则始终打印截断的堆栈跟踪。

记录敏感信息

以日志级别运行 GradleDEBUG可以将安全敏感信息暴露给控制台和构建日志。

这些信息可以包括但不限于:

  • 环境变量
  • 私有存储库凭据
  • 构建缓存和 Gradle 企业凭证
  • 插件门户发布凭据

在公共持续集成服务上运行时,不应使用DEBUG日志级别。公共持续集成服务的构建日志是全球可见的,并且可以公开这些敏感信息。根据您组织的威胁模型,在私有 CI 中记录敏感凭据也可能是一个漏洞。请与您组织的安全团队讨论此问题。

一些 CI 提供者试图从日志中清除敏感凭据;但是,这将是不完美的,并且通常只会清除与预配置机密完全匹配的内容。

如果您认为 Gradle 插件可能会暴露敏感信息,请联系security@gradle.com寻求披露帮助。

编写自己的日志消息

登录构建文件的一个简单选项是将消息写入标准输出。Gradle 将任何写入标准输出的内容重定向到其QUIET日志级别的日志系统。

示例 1. 使用 stdout 写入日志消息

build.gradle

println 'A message which is logged at QUIET level'

Gradle 还为logger构建脚本提供了一个属性,它是Logger的一个实例。该接口扩展了 SLF4JLogger接口,并为其添加了一些 Gradle 特定的方法。下面是如何在构建脚本中使用它的示例:

示例 2. 编写您自己的日志消息

build.gradle

logger.quiet('An info log message which is always logged.')
logger.error('An error log message.')
logger.warn('A warning log message.')
logger.lifecycle('A lifecycle info log message.')
logger.info('An info log message.')
logger.debug('A debug log message.')
logger.trace('A trace log message.') // Gradle never logs TRACE level logs

使用典型的 SLF4J 模式将占位符替换为实际值作为日志消息的一部分。

示例 3. 使用占位符编写日志消息

build.gradle

logger.info('A {} log message', 'info')

您还可以从构建中使用的其他类(buildSrc例如目录中的类)中挂钩到 Gradle 的日志记录系统。只需使用 SLF4J 记录器。您可以像使用构建脚本中提供的记录器一样使用此记录器。

示例 4. 使用 SLF4J 编写日志消息

build.gradle

import org.slf4j.LoggerFactory

def slf4jLogger = LoggerFactory.getLogger('some-logger')
slf4jLogger.info('An info log message logged using SLF4j')

从外部工具和库记录

在内部,Gradle 使用 Ant 和 Ivy。两者都有自己的日志系统。Gradle 将他们的日志输出重定向到 Gradle 日志系统。从 Ant/Ivy 日志级别到 Gradle 日志级别之间存在 1:1 映射,但 Ant/Ivy 日志级别除外TRACE,它映射到 GradleDEBUG日志级别。这意味着默认的 Gradle 日志级别不会显示任何 Ant/Ivy 输出,除非它是错误或警告。

有许多工具仍然使用标准输出进行日志记录。默认情况下,Gradle 将标准输出重定向到QUIET日志级别,将标准错误重定向到该ERROR级别。此行为是可配置的。项目对象提供了一个LoggingManager,它允许您更改在评估构建脚本时将标准输出或错误重定向到的日志级别。

示例 5. 配置标准输出捕获

build.gradle

logging.captureStandardOutput LogLevel.INFO
println 'A message which is logged at INFO level'

要在任务执行期间更改标准输出或错误的日志级别,任务还提供了一个LoggingManager

示例 6. 为任务配置标准输出捕获

build.gradle

tasks.register('logInfo') {
    logging.captureStandardOutput LogLevel.INFO
    doFirst {
        println 'A task message which is logged at INFO level'
    }
}

Gradle 还提供与 Java Util Logging、Jakarta Commons Logging 和 Log4j 日志工具包的集成。您的构建类使用这些日志记录工具包编写的任何日志消息都将被重定向到 Gradle 的日志记录系统。

更改 Gradle 记录的内容

您可以用自己的替换 Gradle 的大部分日志记录 UI。例如,如果您想以某种方式自定义 UI - 记录更多或更少的信息,或更改格式,您可能会这样做。您使用Gradle.useLogger(java.lang.Object)方法替换日志记录。这可以通过构建脚本、初始化脚本或嵌入 API 访问。请注意,这会完全禁用 Gradle 的默认输出。下面是一个示例初始化脚本,它更改了任务执行和构建完成的记录方式。

示例 7. 自定义 Gradle 记录的内容

customLogger.init.gradle

useLogger(new CustomEventLogger())

class CustomEventLogger extends BuildAdapter implements TaskExecutionListener {

    void beforeExecute(Task task) {
        println "[$task.name]"
    }

    void afterExecute(Task task, TaskState state) {
        println()
    }
    
    void buildFinished(BuildResult result) {
        println 'build completed'
        if (result.failure != null) {
            result.failure.printStackTrace()
        }
    }
}

$ gradle -I customLogger.init.gradle build

> Task :compile
[compile]
compiling source


> Task :testCompile
[testCompile]
compiling test source


> Task :test
[test]
running unit tests


> Task :build
[build]

build completed
3 actionable tasks: 3 executed

您的记录器可以实现下面列出的任何侦听器接口。当您注册一个记录器时,只有它实现的接口的记录被替换。其他接口的日志记录保持不变。您可以在构建生命周期事件中找到有关侦听器接口的更多信息。

0

评论区