文件操作
文件操作几乎是所有 Gradle 构建的基础。它们涉及处理源文件、管理文件依赖项和生成报告。Gradle 提供了一个强大的 API,可以简化这些操作,使开发人员能够轻松执行必要的文件任务。
硬编码路径和惰性执行
最佳实践是避免在构建脚本中使用硬编码路径。
除了避免硬编码路径外,Gradle 还鼓励在构建脚本中采用惰性执行。这意味着任务和操作应该推迟到实际需要时才执行,而不是急切地执行。
本章中的许多示例都使用硬编码路径作为字符串字面量。这使得它们易于理解,但这不是好的实践。问题在于路径经常会改变,需要更改的地方越多,就越有可能遗漏一个并破坏构建。
在可能的情况下,您应该按此顺序使用任务、任务属性和项目属性来配置文件路径。
例如,如果您创建一个任务来打包 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 基于标准 Java File
类构建,该类表示单个文件的位置并提供处理路径集合的 API。
使用 ProjectLayout
ProjectLayout
类用于访问项目中的各种目录和文件。它提供了检索项目目录、构建目录、设置文件和项目文件结构中其他重要位置的路径的方法。当您需要在构建脚本或插件中使用不同项目路径中的文件时,此类别特别有用。
val archivesDirPath = layout.buildDirectory.dir("archives")
def archivesDirPath = layout.buildDirectory.dir('archives')
您可以在服务中了解有关 ProjectLayout
类的更多信息。
使用 Project.file()
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()
方法将始终将相对路径转换为相对于当前项目目录(可能是子项目)的路径。
使用 ProjectLayout.settingsDirectory()
要使用相对于设置目录的路径,请访问 Project.layout
,从中检索 settingsDirectory
,并构造一个绝对路径。
例如:
val configFile = layout.settingsDirectory.file("shared/config.xml").asFile
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
:
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 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 src/file1.txt src/file2.txt src/file5.txt
如果 collection
随时发生变化,无论是通过添加或删除文件,那么 textFiles
将立即反映变化,因为它也是一个动态集合。请注意,您传递给 filter()
的闭包将 File
作为参数并应返回布尔值。
理解到文件集合的隐式转换
Gradle 中的许多对象都具有接受一组输入文件的属性。例如,JavaCompile 任务有一个 source
属性,用于定义要编译的源文件。您可以使用 files() 方法支持的任何类型设置此属性的值,如 API 文档中所述。这意味着您可以,例如,将属性设置为 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() }
}
由于这是一个常见的约定,我们建议您在自己的自定义任务中遵循它。具体来说,如果您计划添加一个方法来配置基于集合的属性,请确保该方法是追加而不是替换值。
使用 FileTree
文件树是一个文件集合,它保留所包含文件的目录结构并具有 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()
返回一个 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 不支持在执行阶段更改默认排除项。 |
您可以使用文件树执行与文件集合相同的许多操作:
-
迭代它们(深度优先)
-
筛选它们(使用 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 中的文件复制主要使用 CopySpec
,这是一种方便管理项目构建过程中源代码、配置文件和其他资产等资源的机制。
理解 CopySpec
CopySpec
是一个复制规范,允许您定义要复制哪些文件、从何处复制以及复制到何处。它提供了一种灵活且富有表现力的方式来指定复杂的文件复制操作,包括根据模式筛选文件、重命名文件以及根据各种条件包含/排除文件。
CopySpec
实例在 Copy
任务中使用,以指定要复制的文件和目录。
CopySpec
具有两个重要属性:
-
它独立于任务,允许您在构建中共享复制规范。
-
它是分层的,在整个复制规范中提供细粒度控制。
1. 共享复制规范
考虑一个构建,其中有几个任务复制项目的静态网站资源或将其添加到归档。一个任务可能将资源复制到本地 HTTP 服务器的文件夹中,另一个任务可能将其打包到分发中。您可以手动指定每次需要时文件位置和适当的包含,但更容易出现人为错误,导致任务之间不一致。
一个解决方案是 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()
规范。这不仅适用于包含,还适用于排除、文件重命名和文件内容筛选。
2. 使用子规范
如果您只使用单个复制规范,文件筛选和重命名将应用于所有复制的文件。有时,这是您想要的,但并非总是如此。考虑以下示例,它将文件复制到 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()
将文件复制到目标子目录。两种方法都可以接受,但您应该创建并遵循约定以确保构建文件之间的一致性。
不要混淆您的 |
最后需要注意的一点是,子复制规范从其父级继承其目标路径、包含模式、排除模式、复制操作、名称映射和过滤器。因此,请注意配置的位置。
使用 Sync
任务
Sync 任务继承自 Copy
任务,它将源文件复制到目标目录,然后从目标目录中删除它未复制的任何文件。它将其目录的内容与其源同步。
这对于安装您的应用程序、创建归档文件的解压副本或维护项目依赖项的副本等操作很有用。
以下是一个示例,它在 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
任务
您可以创建一个 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")
}
然后使用文件和目录路径来指定使用 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
表达式。
复制目录层次结构
您可能需要复制文件以及它们所在的目录结构。当您将目录指定为 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()
,而上一节中的指令适用于整个任务。复制规范中这些不同的粒度级别使您能够轻松处理大多数遇到的要求。
理解文件复制
Gradle 中复制文件的基本过程很简单:
-
定义一个类型为 Copy 的任务
-
指定要复制的文件(和可能的目录)
-
指定复制文件的目标
但这种表面上的简单性隐藏了一个丰富的 API,它允许对复制哪些文件、它们到哪里以及它们在复制时发生什么进行细粒度控制——例如,文件重命名和文件内容标记替换都是可能的。
-
一个 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 中)或 Provider(在 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()
的惰性配置与子规范不同,即使语法相似。请注意参数的数量以区分它们。
在您自己的任务中复制文件
如本文所述,在执行时使用 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()
方法的自定义任务应声明与复制操作相关的必要输入和输出。
重命名文件
Gradle 中的文件重命名可以使用 CopySpec
API 完成,该 API 提供了在复制文件时重命名文件的方法。
使用 Copy.rename()
如果您的构建使用和生成的文件有时名称不合适,您可以在复制它们时重命名这些文件。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()
上的子规范的一部分来重命名文件子集。
使用 Copyspec.rename{}
有关如何在复制时重命名文件的示例为您提供了执行此操作所需的大部分信息。它演示了两种重命名选项:
-
使用正则表达式
-
使用闭包
正则表达式是一种灵活的重命名方法,特别是 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.uppercase()
}
}
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
。请注意,闭包将为每个复制的文件执行,因此请尽可能避免耗费大量资源的操作。
筛选文件
Gradle 中的文件筛选涉及根据某些条件选择性地包含或排除文件。
使用 CopySpec.include()
和 CopySpec.exclude()
您可以通过 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 中筛选文件内容涉及将文件中的占位符或令牌替换为动态值。
使用 CopySpec.filter()
在复制文件时转换文件内容涉及使用令牌替换的基本模板化、删除文本行,甚至使用成熟的模板引擎进行更复杂的筛选。
以下示例演示了几种形式的筛选,包括使用 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 风格令牌)替换为您定义的值。
使用 CopySpec.expand()
expand()
方法将源文件视为 Groovy 模板,它评估和扩展 ${expression}
形式的表达式。
您可以传入属性名称和值,然后这些属性名称和值会在源文件中展开。expand()
允许进行比基本令牌替换更多的工作,因为嵌入式表达式是完整的 Groovy 表达式。
在读取和写入文件时指定字符集是一种好的做法。否则,转换对于非 ASCII 文本将无法正常工作。您可以使用 CopySpec.setFilteringCharset(String) 属性配置字符集。如果未指定,则使用 JVM 默认字符集,这可能与您想要的字符集不同。 |
设置文件权限
Gradle 中的文件权限设置涉及为构建过程中创建或修改的文件或目录指定权限。
使用 CopySpec.filePermissions{}
对于任何涉及复制文件的 CopySpec
,无论是 Copy
任务本身还是任何子规范,您都可以通过 CopySpec.filePermissions {} 配置块明确设置目标文件将具有的权限。
使用 CopySpec.dirPermissions{}
您也可以为目录执行相同的操作,独立于文件,通过 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
)
还可以为特定文件设置权限。请参阅以下示例:
tasks.register<Copy>("specificPermissions") {
from("src/main/webapp")
into(layout.buildDirectory.dir("explodedWarWithScript"))
eachFile {
if (name == "script.sh") {
permissions {
user {
execute = true
}
}
}
}
}
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…) 方法支持的方式指定要删除的文件和目录。
例如,以下任务删除构建输出目录的全部内容:
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"
}
}
使用 Project.delete()
Project.delete(org.gradle.api.Action) 方法可以删除文件和目录。
此方法接受一个或多个参数,表示要删除的文件或目录。
例如,以下任务删除构建输出目录的全部内容:
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 的角度来看,将文件打包到归档文件中实际上是一种复制,其中目标是归档文件而不是文件系统上的目录。创建归档文件看起来很像复制,具有所有相同的功能。
使用 Zip
、Tar
或 Jar
任务
最简单的情况涉及归档目录的全部内容,此示例通过创建 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 将它们视为如此。这就是为什么处理归档类似于处理文件和目录的原因。
开箱即用,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 有几个关于归档文件命名和创建位置的约定,这些约定基于您的项目使用的插件。主要约定由 基本插件提供,该插件默认在 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 archive-naming-1.0.zip build/distributions build/distributions/archive-naming-1.0.zip
请注意,归档名称不来源于创建它的任务名称。
如果您想更改生成的归档文件的名称和位置,可以为相应任务的 archiveFileName
和 destinationDirectory
属性提供值。这些值将覆盖任何其他将应用的约定。
或者,您可以使用 AbstractArchiveTask.getArchiveFileName() 提供的默认归档名称模式:[archiveBaseName]-[archiveAppendix]-[archiveVersion]-[archiveClassifier].[archiveExtension]。您可以分别在任务上设置这些属性。请注意,基本插件使用项目名称作为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 customName-1.0.zip
您还可以通过在 base
块中配置 archivesName
属性来覆盖构建中所有归档任务的默认 archiveBaseName
值,如下面的示例所示。该块由 base
插件定义并由 BasePluginExtension
类支持。
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 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
。您当然可以将其设置为自定义扩展名。
在多个归档文件之间共享内容
如上文 CopySpec
部分所述,您可以使用 Project.copySpec(org.gradle.api.Action) 方法在归档文件之间共享内容。
将归档文件用作文件树
归档文件是打包到单个文件中的目录和文件层次结构。换句话说,它是文件树的一种特殊情况,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 9 开始,归档文件默认是可重现的。 |
理想情况下,归档文件应在不同的机器上精确地(字节对字节)重新创建。从源代码构建工件应该产生相同的结果,无论何时何地构建。这对于 reproducible-builds.org 等项目是必需的。
重现相同的字节对字节归档通常会带来一些挑战,因为归档中文件的顺序可能受到底层文件系统的影响。每次从源代码构建 ZIP、TAR、JAR、WAR 或 EAR 时,归档内部文件的顺序可能会改变。磁盘上的文件也可能具有不同的时间戳或权限,具体取决于环境。
从 Gradle 9.0 开始,所有 AbstractArchiveTask(例如 Jar
、Zip
)任务默认生成可重现的归档文件:
-
归档中的文件顺序是确定性的
-
文件具有固定的时间戳(确切值取决于归档类型)
-
目录具有固定的权限,设置为
0755
-
文件具有固定的权限,设置为
0644
保留文件系统定义的方面
如果需要,您可以恢复可重现归档文件的各个方面。
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)
}
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() 以保留文件系统中的权限。 |
如果文件系统已经成为权限的真实来源(例如,通过版本控制工具),您可以通过配置属性来保留所有归档任务的权限:
org.gradle.archives.use-file-system-permissions=true
归档文件中文件的权限
Gradle 归档文件中的文件权限设置方式与复制文件时相同,详见设置文件权限。主要区别在于,归档任务默认将归档文件中文件和目录的权限设置为固定值,为了可重现性,文件权限为 0644
,目录权限为 0755
。
我们认识到两种主要情况,用户可能希望更改归档文件中文件和目录的权限:
-
文件系统已是权限的真实来源的情况。对于此类配置,请参阅保留文件系统定义的方面部分。
-
归档文件中文件和目录的权限应设置为特定值的情况,例如为脚本文件设置可执行位。对于此类配置,请参阅设置文件权限部分。
解压归档
归档实际上是自包含的文件系统,因此解压它们就是将文件从该文件系统复制到本地文件系统——甚至复制到另一个归档中。Gradle 通过提供一些包装函数来实现这一点,这些函数将归档文件作为分层的文件集合(文件树)提供。
使用 Project.zipTree
和 Project.tarTree
两个相关的函数是 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 中创建“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
任务对此进行了演示:
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 任务都确保,如果需要,使用此机制创建它们所需的任何输出目录。
使用 File.mkdirs
和 Files.createDirectories
在需要手动创建目录的情况下,您可以在构建脚本或自定义任务实现中使用标准 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
任务将自动在给定路径中创建所有必要的目录。如果目录已存在,它将不执行任何操作。
使用 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():
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")
}
将单个文件部署到应用服务器
将单个文件部署到应用服务器通常是指将打包的应用程序工件(例如 WAR 文件)传输到应用服务器的部署目录的过程。
使用 Copy
任务
与应用服务器一起工作时,您可以使用 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")
}