使用文件
复制单个文件
您可以通过创建 Gradle 内置 Copy 任务的实例并使用文件的位置和要放置文件的位置对其进行配置来复制文件。此示例模拟将生成的报告复制到将打包到存档(如 ZIP 或 TAR)中的目录中
tasks.register<Copy>("copyReport") {
from(layout.buildDirectory.file("reports/my-report.pdf"))
into(layout.buildDirectory.dir("toArchive"))
}
tasks.register('copyReport', Copy) {
from layout.buildDirectory.file("reports/my-report.pdf")
into layout.buildDirectory.dir("toArchive")
}
ProjectLayout 类用于查找相对于当前项目的某个文件或目录路径。这是让构建脚本无论项目路径如何都能正常运行的常用方法。然后使用文件和目录路径来指定要使用 Copy.from(java.lang.Object…) 复制的文件以及使用 Copy.into(java.lang.Object) 将其复制到的目录。
虽然硬编码路径可以简化示例,但它们也会使构建变得脆弱。最好使用可靠的单一真实来源,例如任务或共享项目属性。在以下修改后的示例中,我们使用在其他位置定义的报告任务,该任务将报告的位置存储在其 outputFile
属性中
tasks.register<Copy>("copyReport2") {
from(myReportTask.flatMap { it.outputFile })
into(archiveReportsTask.flatMap { it.dirToArchive })
}
tasks.register('copyReport2', Copy) {
from myReportTask.outputFile
into archiveReportsTask.dirToArchive
}
我们还假设报告将由 archiveReportsTask
存档,该任务为我们提供了将被存档的目录,因此我们希望将报告副本放在该目录中。
复制多个文件
您可以通过向 from()
提供多个参数,非常轻松地将之前的示例扩展到多个文件
tasks.register<Copy>("copyReportsForArchiving") {
from(layout.buildDirectory.file("reports/my-report.pdf"), layout.projectDirectory.file("src/docs/manual.pdf"))
into(layout.buildDirectory.dir("toArchive"))
}
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
tasks.register<Copy>("copyPdfReportsForArchiving") {
from(layout.buildDirectory.dir("reports"))
include("*.pdf")
into(layout.buildDirectory.dir("toArchive"))
}
tasks.register('copyPdfReportsForArchiving', Copy) {
from layout.buildDirectory.dir("reports")
include "*.pdf"
into layout.buildDirectory.dir("toArchive")
}
需要注意的一点是,如下面的图表所示,只有直接位于 reports
目录中的 PDF 才会被复制
您可以通过使用 Ant 风格的 glob 模式 (**/*
) 来包含子目录中的文件,如在此更新的示例中所示
tasks.register<Copy>("copyAllPdfReportsForArchiving") {
from(layout.buildDirectory.dir("reports"))
include("**/*.pdf")
into(layout.buildDirectory.dir("toArchive"))
}
tasks.register('copyAllPdfReportsForArchiving', Copy) {
from layout.buildDirectory.dir("reports")
include "**/*.pdf"
into layout.buildDirectory.dir("toArchive")
}
此任务具有以下效果
需要注意的一点是,像这样的深度过滤器会产生一个副作用,即复制 reports
下的目录结构以及文件。如果你只想复制文件而不复制目录结构,则需要使用显式的 fileTree(dir) { includes }.files
表达式。我们将在 文件树 部分详细讨论文件树和文件集合之间的差异。
在处理 Gradle 构建中的文件操作时,这只是你可能会遇到的行为变化之一。幸运的是,Gradle 为几乎所有这些用例提供了优雅的解决方案。阅读本章后面的深入部分,详细了解 Gradle 中的文件操作的工作原理以及你可以用来配置它们的选项。
复制目录层次结构
你可能需要复制文件,以及它们所在的目录结构。当你将目录指定为 from()
参数时,这是默认行为,如下例所示,该示例将 reports
目录中的所有内容(包括其所有子目录)复制到目标
tasks.register<Copy>("copyReportsDirForArchiving") {
from(layout.buildDirectory.dir("reports"))
into(layout.buildDirectory.dir("toArchive"))
}
tasks.register('copyReportsDirForArchiving', Copy) {
from layout.buildDirectory.dir("reports")
into layout.buildDirectory.dir("toArchive")
}
用户难以控制的是有多少目录结构进入目标。在上面的示例中,你获得的是 toArchive/reports
目录,还是 reports
中的所有内容都直接进入 toArchive
?答案是后者。如果目录是 from()
路径的一部分,那么它不会出现在目标中。
那么如何确保复制 reports
本身,但不复制 ${layout.buildDirectory}
中的任何其他目录?答案是将其添加为包含模式
tasks.register<Copy>("copyReportsDirForArchiving2") {
from(layout.buildDirectory) {
include("reports/**")
}
into(layout.buildDirectory.dir("toArchive"))
}
tasks.register('copyReportsDirForArchiving2', Copy) {
from(layout.buildDirectory) {
include "reports/**"
}
into layout.buildDirectory.dir("toArchive")
}
你将获得与之前相同的行为,除了目标中多了一层目录,即 toArchive/reports
。
需要注意的一点是,include()
指令仅适用于 from()
,而上一部分中的指令适用于整个任务。复制规范中的这些不同粒度级别使你可以轻松处理你将遇到的大多数要求。你可以在 子规范 部分中了解更多相关信息。
创建存档(zip、tar 等)
从 Gradle 的角度来看,将文件打包到存档中实际上是一种复制,其中目标是存档文件,而不是文件系统上的目录。这意味着创建存档看起来很像复制,具有所有相同的功能!
最简单的情况涉及存档目录的全部内容,此示例通过创建 toArchive
目录的 ZIP 来演示这一点
tasks.register<Zip>("packageDistribution") {
archiveFileName = "my-distribution.zip"
destinationDirectory = layout.buildDirectory.dir("dist")
from(layout.buildDirectory.dir("toArchive"))
}
tasks.register('packageDistribution', Zip) {
archiveFileName = "my-distribution.zip"
destinationDirectory = layout.buildDirectory.dir('dist')
from layout.buildDirectory.dir("toArchive")
}
请注意,我们如何指定存档的目标和名称,而不是 into()
:两者都是必需的。您通常不会看到它们被明确设置,因为大多数项目都应用了 基本插件。它为这些属性提供了一些约定值。下一个示例演示了这一点,您可以在 存档命名 部分中了解有关约定的更多信息。
最常见的场景之一涉及将文件复制到存档的指定子目录中。例如,假设您想将所有 PDF 打包到存档根目录中的 docs
目录中。此 docs
目录在源位置中不存在,因此您必须在存档中创建它。您可以通过仅为 PDF 添加 into()
声明来执行此操作
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")
}
}
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()
规范中使用该文件树,如下所示
tasks.register<Copy>("unpackFiles") {
from(zipTree("src/resources/thirdPartyResources.zip"))
into(layout.buildDirectory.dir("resources"))
}
tasks.register('unpackFiles', Copy) {
from zipTree("src/resources/thirdPartyResources.zip")
into layout.buildDirectory.dir("resources")
}
更高级的处理可以通过 eachFile() 方法进行处理。例如,您可能需要将存档的不同子树解压到目标目录中的不同路径中。以下示例使用该方法将存档的 libs
目录中的文件解压到根目标目录中,而不是解压到 libs
子目录中
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"))
}
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 使这种方法易于实现。考虑目标:将其他 JAR 文件的内容复制到应用程序 JAR 中。您只需要 Project.zipTree(java.lang.Object) 方法和 Jar 任务,如下面的示例中 uberJar
任务所示
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) }
})
}
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 任务都确保在必要时使用此机制创建它们所需的任何输出目录。
在需要手动创建目录的情况下,您可以在构建脚本或自定义任务实现中使用标准的Files.createDirectories
或File.mkdirs
方法。这是一个简单的示例,它在项目文件夹中创建单个images
目录
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())
}
}
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
任务将自动在给定路径中创建所有必要的目录,如果目录已存在,则不执行任何操作。
移动文件和目录
Gradle 没有用于移动文件和目录的 API,但您可以使用Apache Ant 集成轻松地做到这一点,如本示例所示
tasks.register("moveReports") {
// Store the build directory into a variable to avoid project reference in the configuration cache
val dir = buildDir
doLast {
ant.withGroovyBuilder {
"move"("file" to "${dir}/reports", "todir" to "${dir}/toArchive")
}
}
}
tasks.register('moveReports') {
// Store the build directory into a variable to avoid project reference in the configuration cache
def dir = buildDir
doLast {
ant.move file: "${dir}/reports",
todir: "${dir}/toArchive"
}
}
这不是一个常见的要求,并且应该谨慎使用,因为您会丢失信息并且很容易破坏构建。通常最好复制目录和文件。
在复制时重命名文件
您的构建使用和生成的文件有时没有合适的名称,在这种情况下,您希望在复制它们时重命名这些文件。Gradle 允许您使用rename()
配置作为复制规范的一部分来执行此操作。
以下示例从具有它的任何文件的名称中删除“-staging”标记
tasks.register<Copy>("copyFromStaging") {
from("src/main/webapp")
into(layout.buildDirectory.dir("explodedWar"))
rename("(.+)-staging(.+)", "$1$2")
}
tasks.register('copyFromStaging', Copy) {
from "src/main/webapp"
into layout.buildDirectory.dir('explodedWar')
rename '(.+)-staging(.+)', '$1$2'
}
您可以为此使用正则表达式,如上面的示例所示,或使用闭包来使用更复杂的逻辑来确定目标文件名。例如,以下任务截断文件名
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"))
}
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…)方法支持的方式指定要删除的文件和目录。
例如,以下任务删除构建输出目录的全部内容
tasks.register<Delete>("myClean") {
delete(buildDir)
}
tasks.register('myClean', Delete) {
delete buildDir
}
如果您希望更好地控制要删除的文件,则不能像复制文件那样使用包含和排除。相反,您必须使用FileCollection
和FileTree
的内置过滤机制。以下示例就是这样做的,以清除源目录中的临时文件
tasks.register<Delete>("cleanTempFiles") {
delete(fileTree("src").matching {
include("**/*.tmp")
})
}
tasks.register('cleanTempFiles', Delete) {
delete fileTree("src").matching {
include "**/*.tmp"
}
}
您将在下一部分中了解有关文件集合和文件树的更多信息。
深入了解文件路径
要对文件执行某些操作,您需要知道它所在的位置,而文件路径提供了该信息。Gradle 构建基于标准 Java File
类,该类表示单个文件的位置,并提供了用于处理路径集合的新 API。本部分将向您展示如何使用 Gradle API 为任务和文件操作指定文件路径。
但首先,请注意在构建中使用硬编码文件路径。
关于硬编码文件路径
本章中的许多示例都使用硬编码路径作为字符串文字。这使得它们易于理解,但对于实际构建来说并不是好习惯。问题在于路径经常更改,并且您需要更改它们的次数越多,您就越有可能错过一个并破坏构建。
在可能的情况下,您应该使用任务、任务属性和 项目属性(按此优先顺序)来配置文件路径。例如,如果您要创建一个任务来打包 Java 应用程序的已编译类,则应争取实现类似以下内容
val archivesDirPath = layout.buildDirectory.dir("archives")
tasks.register<Zip>("packageClasses") {
archiveAppendix = "classes"
destinationDirectory = archivesDirPath
from(tasks.compileJava)
}
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) 方法来指定单个文件或目录的位置。相对路径相对于项目目录解析,而绝对路径保持不变。
除非传递给 |
以下是一些使用 file()
方法和不同类型参数的示例
// 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"))
// 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() 属性来构建绝对路径,如下所示
val configFile = file("$rootDir/shared/config.xml")
File configFile = file("$rootDir/shared/config.xml")
假设您正在 dev/projects/AcmeHealth
目录中处理多项目构建。您在要修复的库的构建中使用了上面的示例,位于 AcmeHealth/subprojects/AcmePatientRecordLib/build.gradle
。文件路径将解析为 dev/projects/AcmeHealth/shared/config.xml
的绝对版本。
file()
方法可用于配置具有 File
类型属性的任何任务。然而,许多任务处理多个文件,因此我们接下来看看如何指定文件集。
文件集合
文件集合只是一组文件路径,由 FileCollection 接口表示。任何文件路径。重要的是要理解,文件路径不必以任何方式相关,因此它们不必位于同一目录中,甚至不必具有共享的父目录。您还会发现 Gradle API 的许多部分都使用 FileCollection
,例如本章后面讨论的复制 API 和 依赖项配置。
指定文件集合的推荐方法是使用 ProjectLayout.files(java.lang.Object...) 方法,该方法返回一个 FileCollection
实例。此方法非常灵活,允许您传递多个字符串、File
实例、字符串集合、File
集合等。如果任务具有 已定义的输出,您甚至可以将任务作为参数传递。在参考指南中了解所有受支持的参数类型。
files() 正确处理相对路径和 File(relative path) 实例,将它们解析为相对于项目目录的路径。
|
与 Project.file(java.lang.Object) 方法类似(在 上一节 中介绍),所有相对路径均相对于当前项目目录进行评估。以下示例演示了可使用的各种参数类型 — 字符串、File
实例、列表和 Path
val collection: FileCollection = layout.files(
"src/file1.txt",
File("src/file2.txt"),
listOf("src/file3.csv", "src/file4.csv"),
Paths.get("src", "file5.txt")
)
FileCollection collection = layout.files('src/file1.txt',
new File('src/file2.txt'),
['src/file3.csv', 'src/file4.csv'],
Paths.get('src', 'file5.txt'))
文件集合在 Gradle 中具有一些重要属性。它们可以
-
延迟创建
-
进行迭代
-
进行筛选
-
进行组合
文件集合的延迟创建在需要在构建运行时评估构成集合的文件时非常有用。在以下示例中,我们查询文件系统以找出特定目录中存在哪些文件,然后将这些文件制成文件集合
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) }
}
}
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
的输出> 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>
、String
、FileCollection
等。
对文件集合进行迭代可以通过集合上的 each()
方法(在 Groovy 中)或 forEach
方法(在 Kotlin 中)来完成,也可以在 for
循环中使用集合。在这两种方法中,文件集合都被视为一组 File
实例,即迭代变量的类型为 File
。
以下示例演示了此类迭代以及如何使用 as
运算符或受支持的属性将文件集合转换为其他类型
// 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")
// 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
之后使用 union
,union
也将包含这些附加文件。different
文件集合也是如此。
实时集合在过滤方面也很重要。如果你想使用文件集合的子集,你可以利用 FileCollection.filter(org.gradle.api.specs.Spec) 方法来确定要“保留”的文件。在以下示例中,我们创建了一个新集合,它仅包含源集合中以 .txt 结尾的文件
val textFiles: FileCollection = collection.filter { f: File ->
f.name.endsWith(".txt")
}
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()
的闭包将 File
作为参数,并应返回一个布尔值。
文件树
文件树是保留其包含的文件的目录结构的文件集合,其类型为 FileTree。这意味着文件树中的所有路径都必须具有共享的父目录。下图重点介绍了在复制文件这一常见情况下文件树和文件集合之间的区别
虽然 FileTree 扩展了 FileCollection (是一种 is-a 关系),但它们的行为确实不同。换句话说,你可以在需要文件集合的任何地方使用文件树,但请记住:文件集合是文件的平面列表/集合,而文件树是文件和目录层次结构。要将文件树转换为平面集合,请使用 FileTree.getFiles() 属性。
|
创建文件树最简单的方法是将文件或目录路径传递给 Project.fileTree(java.lang.Object) 方法。这将在该基本目录中创建所有文件和目录的树(但不包括基本目录本身)。以下示例演示了如何使用基本方法,此外,还演示了如何使用 Ant 风格的模式过滤文件和目录
// 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*/**")
// 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()
的 API 文档,以查看你可以将哪些类型作为基本目录传递。
默认情况下,fileTree()
返回一个 FileTree
实例,该实例应用一些默认排除模式以方便使用 — 事实上与 Ant 中的默认模式相同。有关完整的默认排除列表,请参阅 Ant 手册。
如果这些默认排除被证明有问题的,你可以通过在设置脚本中更改默认排除来解决此问题
import org.apache.tools.ant.DirectoryScanner
DirectoryScanner.removeDefaultExclude("**/.git")
DirectoryScanner.removeDefaultExclude("**/.git/**")
import org.apache.tools.ant.DirectoryScanner
DirectoryScanner.removeDefaultExclude('**/.git')
DirectoryScanner.removeDefaultExclude('**/.git/**')
目前,Gradle 的默认排除项是通过 Ant 的 DirectoryScanner 类配置的。
|
Gradle 不支持在执行阶段更改默认排除项。 |
您可以对文件树执行与文件集合相同的大多数操作
-
迭代它们(深度优先)
-
筛选它们(使用 FileTree.matching(org.gradle.api.Action) 和 Ant 风格模式)
-
合并它们
您还可以使用 FileTree.visit(org.gradle.api.Action) 方法遍历文件树。以下示例演示了所有这些技术
// 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}")
}
// 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 处理存档的方式。您无需使用仅适用于普通文件系统的 fileTree()
方法,而是使用 Project.zipTree(java.lang.Object) 和 Project.tarTree(java.lang.Object) 方法来包装相应类型的存档文件(请注意,JAR、WAR 和 EAR 文件是 ZIP)。这两种方法都返回 FileTree
实例,您可以像使用普通文件树一样使用这些实例。例如,您可以通过将存档的内容复制到文件系统上的某个目录来提取存档的部分或全部文件。或者,您可以将一个存档合并到另一个存档中。
以下是一些创建基于存档的文件树的简单示例
// 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"))
// 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
属性,用于定义要编译的源文件。如 API 文档中所述,可以使用 files() 方法支持的任何类型设置此属性的值。这意味着你可以将属性设置为 File
、String
、集合、FileCollection
甚至闭包或 Provider
。
这是特定任务的一个特性!这意味着对于具有 FileCollection
或 FileTree
属性的任何任务,都不会发生隐式转换。如果你想知道在特定情况下是否发生隐式转换,则需要阅读相关文档,例如相应任务的 API 文档。或者,你可以在构建中显式使用 ProjectLayout.files(java.lang.Object...) 来消除所有疑问。
以下是一些 source
属性可以采用的不同类型的参数示例
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) }
})
}
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() 方法支持的任何类型,如下所示
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() })
}
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 中复制文件的基本过程很简单
-
定义 Copy 类型的任务
-
指定要复制的文件(和潜在的目录)
-
为复制的文件指定目标
但这种明显的简单性隐藏了一个丰富的 API,它允许精细地控制哪些文件被复制、它们被复制到哪里以及在复制过程中对它们发生了什么——例如,重命名文件和令牌替换文件内容都是可能的。
让我们从列表中的最后两项开始,它们构成了所谓的复制规范。这在形式上基于 CopySpec 接口,Copy
任务实现了该接口,并提供了
-
一个 CopySpec.from(java.lang.Object…) 方法来定义要复制的内容
-
一个 CopySpec.into(java.lang.Object) 方法来定义目标
CopySpec
有几个附加的方法允许你控制复制过程,但这两个是唯一必需的。into()
很直接,需要一个目录路径作为其参数,该路径可以是 Project.file(java.lang.Object) 方法支持的任何形式。from()
配置更加灵活。
from()
不仅接受多个参数,还允许几种不同类型的参数。例如,一些最常见的类型是
-
一个
String
— 被视为文件路径,或者如果它以“file://”开头,则为文件 URI -
一个
File
— 用作文件路径 -
一个
FileCollection
或FileTree
— 集合中的所有文件都包含在复制中 -
一个任务 — 包含任务的 已定义输出 的文件或目录
事实上,from()
接受与 Project.files(java.lang.Object…) 相同的所有参数,因此请参阅该方法以获取可接受类型的更详细列表。
还需要考虑的是文件路径所指的事物的类型
-
一个文件 — 文件按原样复制
-
一个目录 — 这实际上被视为文件树:其中的所有内容(包括子目录)都被复制。但是,目录本身不包含在复制中。
-
不存在的文件 — 忽略路径
以下示例使用多个 from()
规范,每个规范具有不同的参数类型。您可能还会注意到,into()
使用闭包(在 Groovy 中)或提供程序(在 Kotlin 中)进行延迟配置 — 这也是 from()
使用的技术
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() })
}
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 文件除外
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")
}
}
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”
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()
}
}
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()
方法相同。
在此上下文中使用正则表达式时,人们会遇到两个常见问题
-
如果您对第一个参数使用斜杠字符串(以“/”分隔),则必须为
rename()
包含括号,如上例所示。 -
最安全的方法是对第二个参数使用单引号,否则您需要在组替换中转义“$”,即
"\$1\$2"
。
第一个问题是一个小麻烦,但斜杠字符串的优点是您不必转义正则表达式中的反斜杠('\')字符。第二个问题源于 Groovy 使用 ${ }
语法在双引号和斜杠字符串中支持嵌入式表达式。
rename()
的闭包语法很简单,可用于处理简单正则表达式无法处理的任何要求。您会得到一个文件名称,并返回该文件的新名称,或者如果您不想更改名称,则返回 null
。请注意,闭包将针对每个已复制的文件执行,因此请尽可能避免昂贵的操作。
筛选文件内容(标记替换、模板化等)
不要与筛选要复制的文件混淆,文件内容筛选允许您在复制文件时转换文件的内容。这可能涉及使用标记替换的基本模板化、删除文本行,甚至使用成熟的模板引擎进行更复杂的筛选。
以下示例演示了多种过滤形式,包括使用 CopySpec.expand(java.util.Map) 方法进行标记替换以及使用 CopySpec.filter(java.lang.Class) 和 Ant 过滤器 的另一种形式
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"
}
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 风格标记)形式的标记。
expand()
方法将源文件视为 Groovy 模板,该模板评估并展开 ${expression}
形式的表达式。您可以传入属性名称和值,然后在源文件中展开这些属性。expand()
允许进行不仅仅是基本标记替换的操作,因为嵌入式表达式是完全成熟的 Groovy 表达式。
在读取和写入文件时指定字符集是一个好习惯,否则转换无法正确处理非 ASCII 文本。您可以使用 CopySpec.setFilteringCharset(String) 属性配置字符集。如果未指定,则使用 JVM 默认字符集,该字符集可能与您想要的字符集不同。 |
设置文件权限
对于涉及复制文件的任何 CopySpec
,无论是 Copy
任务本身还是任何子规范,您都可以通过 CopySpec.filePermissions {} 配置块显式设置目标文件将具有的权限。您还可以通过 CopySpec.dirPermissions {} 配置块独立于文件对目录执行相同的操作。
不显式设置权限将保留原始文件或目录的权限。 |
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---")
}
}
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---')
}
}
有关文件权限的详细说明,请参阅 FilePermissions 和 UserClassFilePermissions。有关示例中使用的便捷方法的详细信息,请参阅 ConfigurableFilePermissions.unix(String)。
即使对文件或目录权限使用空配置块,仍会显式设置它们,只是设置为固定的默认值。实际上,这些配置块中的所有内容都与默认值相关。文件和目录的默认权限不同
-
文件:所有者可读写,组可读,其他可读(0644,rw-r—r--)
-
目录:所有者可读写和执行,组可读和执行,其他可读和执行(0755,rwxr-xr-x)
使用 CopySpec
类
复制规范(或简称复制规范)确定将内容复制到何处,以及在复制过程中对文件执行什么操作。您已在 Copy
和归档任务的配置形式中看到了许多示例。但复制规范有两个值得更详细介绍的属性
-
它们可以独立于任务
-
它们是分层的
第一个属性允许您在构建中共享复制规范。第二个属性在整体复制规范中提供细粒度控制。
共享复制规范
考虑一个构建,其中有几个任务可以复制项目的静态网站资源或将它们添加到归档中。一个任务可能会将资源复制到本地 HTTP 服务器的文件夹中,而另一个任务可能会将它们打包到发行版中。您可以在每次需要时手动指定文件位置和适当的包含项,但人为错误更有可能潜入其中,从而导致任务之间出现不一致。
Gradle 提供的一种解决方案是 Project.copySpec(org.gradle.api.Action) 方法。这允许您在任务外部创建一个复制规范,然后可以使用 CopySpec.with(org.gradle.api.file.CopySpec…) 方法将其附加到适当的任务。以下示例演示了如何执行此操作
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)
}
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
}
copyAssets
和 distApp
任务都将处理 src/main/webapp
下的静态资源,如 webAssetsSpec
所指定。
这可能会让人难以理解,因此最好将 |
如果您遇到想要将相同的复制配置应用于不同文件集的情况,那么您可以直接共享配置块,而无需使用 copySpec()
。以下是一个示例,其中有两个独立的任务,碰巧只想处理图像文件
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)
}
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 容器可用于提供网站的目录结构中
这不是一个简单的复制,因为 WEB-INF
目录及其子目录不存在于项目中,因此必须在复制期间创建它们。此外,我们只希望 HTML 和图像文件直接进入根文件夹 — build/explodedWar
— 并且只有 JavaScript 文件进入 js
目录。因此,我们需要为这两组文件制定单独的筛选模式。
解决方案是使用子规范,它可以应用于 from()
和 into()
声明。以下任务定义完成了必要的工作
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)
}
}
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()
。两种方法都是可以接受的,但您可能希望创建并遵循约定,以确保构建文件的一致性。
不要混淆您的 |
需要注意的最后一件事是,子复制规范从其父级继承其目标路径、包含模式、排除模式、复制操作、名称映射和筛选器。因此,请注意您放置配置的位置。
在您自己的任务中复制文件
在执行时使用 Project.copy 方法(如此处所述)与配置缓存不兼容。一种可能的解决方案是将任务实现为一个适当的类,并使用 FileSystemOperations.copy 方法,如配置缓存章节中所述。
|
有时您可能希望将文件或目录复制为任务的一部分。例如,基于不受支持的归档格式的自定义归档任务可能希望在归档之前将文件复制到临时目录。您仍然希望利用 Gradle 的复制 API,但无需引入额外的 Copy
任务。
解决方案是使用 Project.copy(org.gradle.api.Action) 方法。它的工作方式与 Copy
任务相同,通过使用复制规范对其进行配置。这是一个简单的示例
tasks.register("copyMethod") {
doLast {
copy {
from("src/main/webapp")
into(layout.buildDirectory.dir("explodedWar"))
include("**/*.html")
include("**/*.jsp")
}
}
}
tasks.register('copyMethod') {
doLast {
copy {
from 'src/main/webapp'
into layout.buildDirectory.dir('explodedWar')
include '**/*.html'
include '**/*.jsp'
}
}
}
以上示例演示了基本语法,还强调了使用 copy()
方法的两个主要限制
-
copy()
方法不是 增量 方法。示例的copyMethod
任务将始终执行,因为它没有关于构成任务输入的文件的信息。您必须手动定义任务输入和输出。 -
使用任务作为复制源,即作为
from()
的参数,不会在您的任务和该复制源之间设置自动任务依赖项。因此,如果您将copy()
方法用作任务操作的一部分,则必须显式声明所有输入和输出才能获得正确的行为。
以下示例向您展示如何使用 任务输入和输出的动态 API 来解决这些限制
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")
}
}
}
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
任务的扩展 Sync 任务将源文件复制到目标目录,然后从目标目录中删除其未复制的任何文件。换句话说,它将目录的内容与其源同步。这对于执行诸如安装应用程序、创建存档的展开副本或维护项目依赖项副本等操作非常有用。
这里有一个示例,它维护 build/libs
目录中项目运行时依赖项的副本。
tasks.register<Sync>("libs") {
from(configurations["runtime"])
into(layout.buildDirectory.dir("libs"))
}
tasks.register('libs', Sync) {
from configurations.runtime
into layout.buildDirectory.dir('libs')
}
您还可以在自己的任务中使用 Project.sync(org.gradle.api.Action) 方法执行相同的功能。
将单个文件部署到应用程序服务器
在使用应用程序服务器时,您可以使用 Copy
任务部署应用程序存档(例如 WAR 文件)。由于您部署的是单个文件,因此 Copy
的目标目录是整个部署目录。部署目录有时确实包含不可读的文件,例如命名管道,因此 Gradle 在执行最新检查时可能会遇到问题。为了支持此用例,您可以使用 Task.doNotTrackState()。
plugins {
war
}
tasks.register<Copy>("deployToTomcat") {
from(tasks.war)
into(layout.projectDirectory.dir("tomcat/webapps"))
doNotTrackState("Deployment directory contains unreadable files")
}
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()。
tasks.register<Copy>("installExecutable") {
from("build/my-binary")
into("/usr/local/bin")
doNotTrackState("Installation directory contains unrelated files")
}
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。每种格式都有一个相应的任务类型来创建它们:Zip、Tar、Jar、War 和 Ear。这些任务的工作方式相同,并且都基于复制规范,就像 Copy
任务一样。
创建存档文件本质上是文件复制,其中目标是隐式的,即存档文件本身。以下是一个基本示例,它指定目标存档文件的路径和名称
tasks.register<Zip>("packageDistribution") {
archiveFileName = "my-distribution.zip"
destinationDirectory = layout.buildDirectory.dir("dist")
from(layout.buildDirectory.dir("toArchive"))
}
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 插件 提供,该插件默认为在 layout.buildDirectory.dir("distributions")
目录中创建存档,并且通常使用格式为 [projectName]-[version].[type] 的存档名称。
以下示例来自名为 archive-naming
的项目,因此 myZip
任务创建了名为 archive-naming-1.0.zip
的存档
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))
}
}
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
的输出> 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 插件使用项目名称的约定作为 archiveBaseName,项目版本作为 archiveVersion,存档类型作为 archiveExtension。它不为其他属性提供值。
此示例(来自与上述示例相同的项目)仅配置了 archiveBaseName
属性,覆盖了项目名称的默认值
tasks.register<Zip>("myCustomZip") {
archiveBaseName = "customName"
from("somedir")
doLast {
println(archiveFileName.get())
}
}
tasks.register('myCustomZip', Zip) {
archiveBaseName = 'customName'
from 'somedir'
doLast {
println archiveFileName.get()
}
}
gradle -q myCustomZip
的输出> gradle -q myCustomZip customName-1.0.zip
您还可以通过使用项目属性 archivesBaseName
覆盖构建中所有归档任务的默认 archiveBaseName
值,如下例所示
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())
}
}
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
的输出> gradle -q echoNames Project name: archives-changed-base-name gradle-1.0.zip gradle-wrapper-1.0-src.zip
您可以在 AbstractArchiveTask 的 API 文档中找到所有可能的归档任务属性,但我们在此也总结了主要属性
archiveFileName
—Property<String>
,默认值:archiveBaseName-archiveAppendix-archiveVersion-archiveClassifier.archiveExtension
-
生成归档的完整文件名。如果默认值中的任何属性为空,则会删除其 '-' 分隔符。
archiveFile
—Provider<RegularFile>
,只读,默认值:destinationDirectory/archiveFileName
-
生成归档的绝对文件路径。
destinationDirectory
—DirectoryProperty
,默认值:取决于归档类型-
放置生成归档的目标目录。默认情况下,JAR 和 WAR 会进入
layout.buildDirectory.dir("libs")
。ZIP 和 TAR 会进入layout.buildDirectory.dir("distributions")
。 archiveBaseName
—Property<String>
,默认值:project.name
-
归档文件名中的基本名称部分,通常是项目名称或对其包含内容的一些其他描述性名称。
archiveAppendix
—Property<String>
,默认值:null
-
归档文件名中紧跟基本名称之后的附录部分。它通常用于区分不同形式的内容,例如代码和文档,或者最小发行版与完整或完全发行版。
archiveVersion
—Property<String>
,默认值:project.version
-
归档文件名中的版本部分,通常采用常规项目或产品版本的形式。
archiveClassifier
—Property<String>
,默认值:null
-
存档文件名的分类部分。通常用于区分针对不同平台的存档。
archiveExtension
—Property<String>
,默认值:取决于存档类型和压缩类型-
存档的文件名扩展名。默认情况下,这是根据存档任务类型和压缩类型(如果你正在创建 TAR)设置的。将是以下之一:
zip
、jar
、war
、tar
、tgz
或tbz2
。当然,如果你愿意,可以将其设置为自定义扩展名。
在多个存档之间共享内容
如前所述,你可以使用 Project.copySpec(org.gradle.api.Action) 方法在存档之间共享内容。
可再现构建
有时需要在不同的机器上完全相同地重新创建存档,逐字节相同。你希望确保从源代码构建工件无论何时何地构建都能产生相同的结果。对于 reproducible-builds.org 等项目来说,这是必需的。
重新生成逐字节相同的存档会带来一些挑战,因为存档中文件的顺序受基础文件系统的影响。每次从源代码构建 ZIP、TAR、JAR、WAR 或 EAR 时,存档内文件的顺序都可能发生变化。仅具有不同时间戳的文件也会导致构建到构建的存档发生差异。所有 AbstractArchiveTask(例如 Jar、Zip)任务都附带了 Gradle 支持,用于生成可再现存档。
例如,要使 Zip
任务可再现,你需要将 Zip.isReproducibleFileOrder() 设置为 true
,将 Zip.isPreserveFileTimestamps() 设置为 false
。为了使构建中的所有存档任务可再现,请考虑将以下配置添加到构建文件中
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
tasks.withType(AbstractArchiveTask).configureEach {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
通常,你希望发布存档,以便它可以从另一个项目中使用。此过程在 跨项目发布 中进行描述。