檔案操作幾乎是每個 Gradle 建置的基礎。它們涉及處理原始碼檔案、管理檔案相依性,以及產生報告。Gradle 提供了強大的 API,簡化了這些操作,使開發人員能夠輕鬆執行必要的檔案任務。

硬編碼路徑與延遲性

最佳實務是在建置腳本中避免硬編碼路徑。

除了避免硬編碼路徑外,Gradle 也鼓勵在建置腳本中使用延遲性。這表示任務和操作應延遲到實際需要時才執行,而不是急切地執行。

本章中的許多範例都使用硬編碼路徑作為字串文字。這使得它們易於理解,但並非良好的實務。問題在於路徑經常變更,而您需要變更它們的地方越多,您就越有可能遺漏一個並中斷建置。

在可能的情況下,您應該使用任務、任務屬性和專案屬性(依優先順序排列)來配置檔案路徑。

例如,如果您建立一個任務來封裝 Java 應用程式的已編譯類別,則應使用類似以下的實作

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

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

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

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

    from compileJava
}

compileJava 任務是封裝檔案的來源,而專案屬性 archivesDirPath 儲存封存檔的位置,因為我們可能會在建置中的其他地方使用它。

像這樣直接使用任務作為引數取決於它是否具有定義的輸出,因此並非總是可行。這個範例可以進一步改進,方法是依賴 Java 外掛程式的 destinationDirectory 慣例,而不是覆寫它,但它確實示範了專案屬性的使用。

定位檔案

若要對檔案執行某些動作,您需要知道檔案的位置,而這就是檔案路徑提供的資訊。Gradle 以標準 Java File 類別為基礎,該類別代表單一檔案的位置,並提供用於處理路徑集合的 API。

使用 ProjectLayout

ProjectLayout 類別用於存取專案中的各種目錄和檔案。它提供方法來擷取專案目錄、建置目錄、設定檔以及專案檔案結構中其他重要位置的路徑。當您需要在不同專案路徑的建置腳本或外掛程式中使用檔案時,此類別特別有用

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

您可以在服務中瞭解有關 ProjectLayout 類別的更多資訊。

使用 Project.file()

Gradle 提供了 Project.file(java.lang.Object) 方法,用於指定單一檔案或目錄的位置。

相對路徑會相對於專案目錄解析,而絕對路徑則保持不變。

除非傳遞給 file()files()from() 或其他根據 file()files() 定義的方法,否則永遠不要使用 new File(相對路徑)。否則,這會建立相對於目前工作目錄 (CWD) 的路徑。Gradle 無法保證 CWD 的位置,這表示依賴它的建置可能會隨時中斷。

以下是一些使用 file() 方法和不同類型引數的範例

build.gradle.kts
// Using a relative path
var configFile = file("src/config.xml")

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

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

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

// Using an absolute java.nio.file.Path object
configFile = file(Paths.get(System.getProperty("user.home")).resolve("global-config.xml"))
build.gradle
// Using a relative path
File configFile = file('src/config.xml')

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

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

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

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

如您所見,您可以將字串、File 執行個體和 Path 執行個體傳遞給 file() 方法,所有這些都會產生絕對 File 物件。

在多專案建置的情況下,file() 方法會始終將相對路徑轉換為相對於目前專案目錄的路徑,目前專案目錄可能是子專案。

使用 ProjectLayout.settingsDirectory()

若要使用相對於設定目錄的路徑,請存取 Project.layout,從中擷取 settingsDirectory,並建構絕對路徑。

例如

build.gradle.kts
val configFile = layout.settingsDirectory.file("shared/config.xml").asFile
build.gradle
File configFile = layout.settingsDirectory.file("shared/config.xml").asFile

假設您正在目錄 dev/projects/AcmeHealth 中處理多專案建置。上述建置腳本位於:AcmeHealth/subprojects/AcmePatientRecordLib/build.gradle。絕對檔案路徑將解析為:dev/projects/AcmeHealth/shared/config.xml

dev
├── projects
│   ├── AcmeHealth
│   │   ├── subprojects
│   │   │   ├── AcmePatientRecordLib
│   │   │   │   └── build.gradle
│   │   │   └── ...
│   │   ├── shared
│   │   │   └── config.xml
│   │   └── ...
│   └── ...
└── settings.gradle

請注意,Project 也為多專案建置提供 Project.getRootProject(),在範例中,它將解析為:dev/projects/AcmeHealth/subprojects/AcmePatientRecordLib

使用 FileCollection

檔案集合只是一組由 FileCollection 介面表示的檔案路徑。

路徑集合可以是任何檔案路徑。檔案路徑不必以任何方式相關,因此它們不必位於同一個目錄中或具有共用的父目錄。

指定檔案集合的建議方法是使用 ProjectLayout.files(java.lang.Object...) 方法,它會傳回 FileCollection 執行個體。這種彈性的方法可讓您傳遞多個字串、File 執行個體、字串集合、File 集合等等。如果任務具有定義的輸出,您也可以將任務作為引數傳遞。

files() 會正確處理相對路徑和 File(相對路徑) 執行個體,將它們解析為相對於專案目錄。

Project.file(java.lang.Object) 方法(在前一節中涵蓋)一樣,所有相對路徑都會相對於目前專案目錄評估。以下範例示範了您可以使用的各種引數類型 — 字串、File 執行個體、清單或 Path

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

檔案集合在 Gradle 中具有重要屬性。它們可以是

  • 延遲建立

  • 可迭代

  • 已篩選

  • 已合併

當在建置執行時評估組成集合的檔案時,檔案集合的延遲建立非常有用。在以下範例中,我們查詢檔案系統以找出特定目錄中存在哪些檔案,然後將這些檔案建立為檔案集合

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

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

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

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

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

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

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

延遲建立的關鍵是將閉包(在 Groovy 中)或 Provider(在 Kotlin 中)傳遞給 files() 方法。您的閉包或提供者必須傳回 files() 接受的類型的值,例如 List<File>StringFileCollection

迭代檔案集合可以透過集合上的 each() 方法(在 Groovy 中)或 forEach 方法(在 Kotlin 中)或在 for 迴圈中使用集合來完成。在這兩種方法中,檔案集合都被視為一組 File 執行個體,也就是說,您的迭代變數將為 File 類型。

以下範例示範了這種迭代。它也示範了如何使用 as 運算子(或支援的屬性)將檔案集合轉換為其他類型

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

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

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

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

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

您也可以在範例結尾看到如何使用 +- 運算子合併和減去檔案集合來合併檔案集合。結果檔案集合的一個重要功能是它們是即時的。換句話說,當您以這種方式合併檔案集合時,即使來源檔案集合在建置期間變更,結果始終會反映來源檔案集合中目前的內容。

例如,假設上述範例中的 collection 在建立 union 後又獲得一兩個額外檔案。只要您在將這些檔案新增至 collection 後使用 unionunion 也會包含這些額外檔案。different 檔案集合也是如此。

當涉及篩選時,即時集合也很重要。假設您想要使用檔案集合的子集。在這種情況下,您可以利用 FileCollection.filter(org.gradle.api.specs.Spec) 方法來判斷要「保留」哪些檔案。在以下範例中,我們建立一個新的集合,其中僅包含來源集合中以 .txt 結尾的檔案

build.gradle.kts
val textFiles: FileCollection = collection.filter { f: File ->
    f.name.endsWith(".txt")
}
build.gradle
FileCollection textFiles = collection.filter { File f ->
    f.name.endsWith(".txt")
}
$ gradle -q filterTextFiles
src/file1.txt
src/file2.txt
src/file5.txt

如果 collection 在任何時候變更,無論是透過自行新增或移除檔案,textFiles 都會立即反映變更,因為它也是即時集合。請注意,您傳遞給 filter() 的閉包會採用 File 作為引數,並應傳回布林值。

瞭解隱含轉換為檔案集合

Gradle 中的許多物件都具有接受一組輸入檔案的屬性。例如,JavaCompile 任務具有定義要編譯的原始碼檔案的 source 屬性。您可以使用 API 文件中提及的files() 方法支援的任何類型來設定此屬性的值。這表示您可以將屬性設定為 FileString、集合、FileCollection 甚至閉包或 Provider,例如。

這是特定任務的功能!這表示隱含轉換不會發生在任何具有 FileCollectionFileTree 屬性的任務上。如果您想知道隱含轉換是否在特定情況下發生,您需要閱讀相關文件,例如對應任務的 API 文件。或者,您可以明確使用 ProjectLayout.files(java.lang.Object...) 來消除所有疑慮。

以下是一些 source 屬性可以採用的不同類型引數的範例

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

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

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

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

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

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

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

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

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

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

另一件需要注意的是,source 等屬性在核心 Gradle 任務中具有對應的方法。這些方法遵循附加至值集合而非取代值的慣例。同樣地,此方法接受files() 方法支援的任何類型,如下所示

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

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

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

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

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

由於這是一個常見的慣例,我們建議您在自己的自訂任務中遵循它。具體來說,如果您計劃新增一個方法來配置基於集合的屬性,請確保該方法附加而非取代值。

使用 FileTree

檔案樹狀結構是一個檔案集合,它保留了其包含的檔案的目錄結構,並且具有 FileTree 類型。這表示檔案樹狀結構中的所有路徑都必須具有共用的父目錄。下圖重點說明了檔案樹狀結構和檔案集合在複製檔案的典型情況下的區別

file collection vs file tree
雖然 FileTree 延伸了 FileCollection(是一種關係),但它們的行為有所不同。換句話說,您可以在需要檔案集合的任何地方使用檔案樹狀結構,但請記住,檔案集合是檔案的平面清單/集合,而檔案樹狀結構是檔案和目錄階層。若要將檔案樹狀結構轉換為平面集合,請使用 FileTree.getFiles() 屬性。

建立檔案樹狀結構的最簡單方法是將檔案或目錄路徑傳遞給 Project.fileTree(java.lang.Object) 方法。這將在該基礎目錄中建立所有檔案和目錄的樹狀結構(但不包括基礎目錄本身)。以下範例示範了如何使用此方法以及如何使用 Ant 樣式模式篩選檔案和目錄

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

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

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

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

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

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

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

您可以在 PatternFilterable 的 API 文件中查看更多支援模式的範例。

依預設,fileTree() 會傳回一個 FileTree 執行個體,該執行個體為了方便起見而套用一些預設排除模式 — 與 Ant 相同的預設值。如需完整的預設排除清單,請參閱Ant 手冊

如果這些預設排除證明有問題,您可以透過變更設定腳本中的預設排除來解決此問題

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

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

DirectoryScanner.removeDefaultExclude('**/.git')
DirectoryScanner.removeDefaultExclude('**/.git/**')
Gradle 不支援在執行階段變更預設排除。

您可以使用檔案樹狀結構執行許多與檔案集合相同的事情

您也可以使用 FileTree.visit(org.gradle.api.Action) 方法遍歷檔案樹狀結構。以下範例示範了所有這些技術

build.gradle.kts
// Iterate over the contents of a tree
tree.forEach{ file: File ->
    println(file)
}

// Filter a tree
val filtered: FileTree = tree.matching {
    include("org/gradle/api/**")
}

// Add trees together
val sum: FileTree = tree + fileTree("src/test")

// Visit the elements of the tree
tree.visit {
    println("${this.relativePath} => ${this.file}")
}
build.gradle
// Iterate over the contents of a tree
tree.each {File file ->
    println file
}

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

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

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

複製檔案

Gradle 中複製檔案主要使用 CopySpec,這是一種機制,可讓您輕鬆管理專案建置過程中的資源,例如原始碼、組態檔和其他資產。

瞭解 CopySpec

CopySpec 是一個複製規格,可讓您定義要複製的檔案、從何處複製檔案以及將檔案複製到何處。它提供了一種彈性且具表現力的方式來指定複雜的檔案複製操作,包括根據模式篩選檔案、重新命名檔案,以及根據各種準則包含/排除檔案。

CopySpec 執行個體用於 Copy 任務中,以指定要複製的檔案和目錄。

CopySpec 具有兩個重要屬性

  1. 它獨立於任務,可讓您在建置中共用複製規格

  2. 它是階層式的,可在整個複製規格中提供精細的控制

1. 共用複製規格

假設建置包含多個任務,這些任務會複製專案的靜態網站資源或將它們新增到封存檔中。一個任務可能會將資源複製到本機 HTTP 伺服器的資料夾,而另一個任務可能會將它們封裝到散發套件中。您可以手動指定檔案位置和適當的包含項,但人為錯誤更容易悄悄發生,導致任務之間的不一致。

一種解決方案是 Project.copySpec(org.gradle.api.Action) 方法。這可讓您在任務外部建立複製規格,然後可以使用 CopySpec.with(org.gradle.api.file.CopySpec…​) 方法將其附加到適當的任務。以下範例示範了如何完成此操作

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

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

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

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

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

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

    from appClasses
    with webAssetsSpec
}

copyAssetsdistApp 任務都將處理 src/main/webapp 下的靜態資源,如 webAssetsSpec 所指定。

webAssetsSpec 定義的配置將不會套用至 distApp 任務包含的應用程式類別。那是因為 from appClasses 是它自己的子規格,獨立於 with webAssetsSpec

這可能會讓人感到困惑,因此最好將 with() 視為任務中額外的 from() 規格。因此,定義一個獨立的複製規格而沒有至少一個已定義的 from() 是沒有意義的。

假設您遇到一種情況,您想要將相同的複製配置套用至不同的檔案集。在這種情況下,您可以直接共用配置區塊,而無需使用 copySpec()。以下範例有兩個獨立的任務,它們恰好想要僅處理影像檔

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

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

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

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

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

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

    from 'distResources', webAssetPatterns
}

在本例中,我們將複製配置指派給自己的變數,並將其套用至我們想要的任何 from() 規格。這不僅適用於包含,也適用於排除、檔案重新命名和檔案內容篩選。

2. 使用子規格

如果您僅使用單一複製規格,則檔案篩選和重新命名將套用至所有複製的檔案。有時,這正是您想要的,但並非總是如此。請考慮以下範例,該範例將檔案複製到 Java Servlet 容器可用於傳遞網站的目錄結構中

exploded war child copy spec example

這不是直接複製,因為 WEB-INF 目錄及其子目錄在專案中不存在,因此必須在複製期間建立它們。此外,我們只希望 HTML 和影像檔直接進入根資料夾 — build/explodedWar — 並且只有 JavaScript 檔案進入 js 目錄。我們需要針對這兩組檔案使用不同的篩選模式。

解決方案是使用子規格,它可以套用至 from()into() 宣告。以下任務定義完成了必要的工作

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

請注意,src/dist 配置具有巢狀包含規格;它是子複製規格。當然,您可以根據需要在此處新增內容篩選和重新命名。子複製規格仍然是複製規格。

以上範例也示範了如何透過在 from() 上使用子 into() 或在 into() 上使用子 from(),將檔案複製到目的地的子目錄中。這兩種方法都是可以接受的,但您應建立並遵循慣例,以確保建置檔案的一致性。

不要混淆您的 into() 規格。對於正常複製(一個到檔案系統而不是封存檔),應該始終有一個「根」into(),它指定複製的整體目的地目錄。任何其他 into() 都應附加子規格,並且其路徑將相對於根 into()

最後一件需要注意的是,子複製規格會從其父規格繼承其目的地路徑、包含模式、排除模式、複製動作、名稱對應和篩選器。因此,請小心放置您的配置。

使用 Sync 任務

Sync 任務(它延伸了 Copy 任務)將來源檔案複製到目的地目錄,然後從目的地目錄中移除它未複製的任何檔案。它會將目錄的內容與其來源同步。

這對於執行諸如安裝應用程式、建立封存檔的展開副本或維護專案相依性的副本等操作非常有用。

以下範例維護 build/libs 目錄中專案執行階段相依性的副本

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

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

使用 Copy 任務

您可以透過建立 Gradle 內建 Copy 任務的執行個體並使用檔案位置和您要放置它的位置來配置它,從而複製檔案。

此範例模擬將產生的報告複製到將封裝到封存檔(例如 ZIP 或 TAR)中的目錄中

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

然後,檔案和目錄路徑用於使用 Copy.from(java.lang.Object…​) 指定要複製的檔案,以及使用 Copy.into(java.lang.Object) 指定要將其複製到的目錄。

雖然硬編碼路徑使範例變得簡單,但它們會使建置變得脆弱。使用可靠的單一事實來源(例如任務或共用專案屬性)會更好。在以下修改後的範例中,我們使用在其他地方定義的報告任務,該任務的報告位置儲存在其 outputFile 屬性中

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

我們也假設報告將由 archiveReportsTask 封存,這為我們提供了將要封存的目錄,因此也是我們想要放置報告副本的位置。

複製多個檔案

您可以透過為 from() 提供多個引數,非常輕鬆地將先前的範例延伸到多個檔案

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

現在有兩個檔案被複製到封存檔目錄中。

您也可以使用多個 from() 陳述式來執行相同的操作,如深入檔案複製一節的第一個範例所示。

但是,如果您想要複製目錄中的所有 PDF,而無需指定每個 PDF 怎麼辦?若要執行此操作,請將包含和/或排除模式附加到複製規格。在這裡,我們使用字串模式僅包含 PDF

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

需要注意的一件事是,如下圖所示,僅複製直接位於 reports 目錄中的 PDF

copy with flat filter example

您可以透過使用 Ant 樣式 glob 模式 (**/*) 來包含子目錄中的檔案,如此更新後的範例所示

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

此任務具有以下效果

copy with deep filter example

請記住,像這樣深入的篩選具有複製 reports 下的目錄結構和檔案的副作用。如果您想要複製檔案而不複製目錄結構,則必須使用明確的 fileTree(dir) { includes }.files 運算式。

複製目錄階層

您可能需要複製檔案以及它們所在的目錄結構。當您將目錄指定為 from() 引數時,這是預設行為,如下列範例所示,該範例將複製 reports 目錄中的所有內容,包括其所有子目錄,到目的地

build.gradle.kts
tasks.register<Copy>("copyReportsDirForArchiving") {
    from(layout.buildDirectory.dir("reports"))
    into(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('copyReportsDirForArchiving', Copy) {
    from layout.buildDirectory.dir("reports")
    into layout.buildDirectory.dir("toArchive")
}

使用者需要協助的關鍵方面是控制有多少目錄結構進入目的地。在上述範例中,您是否獲得 toArchive/reports 目錄,還是 reports 中的所有內容都直接進入 toArchive?答案是後者。如果目錄是 from() 路徑的一部分,則它不會出現在目的地中。

那麼,您如何確保 reports 本身被複製過去,但不複製 ${layout.buildDirectory} 中的任何其他目錄?答案是將其新增為包含模式

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

您將獲得與之前相同的行為,只是目的地中多了一個目錄層級,即 toArchive/reports

需要注意的一件事是 include() 指令僅適用於 from(),而上一節中的指令適用於整個任務。複製規格中這些不同的細微程度可讓您輕鬆處理您將遇到的大多數需求。

瞭解檔案複製

Gradle 中複製檔案的基本流程很簡單

  • 定義 Copy 類型的任務

  • 指定要複製哪些檔案(以及可能包含的目錄)

  • 指定複製檔案的目的地

但是,這種表面上的簡單性隱藏了豐富的 API,它可以精細地控制複製哪些檔案、它們的去向以及複製時發生的情況 — 例如,檔案的重新命名和檔案內容的符號取代都是可能的。

讓我們從清單中的最後兩項開始,它們涉及 CopySpec。實作 Copy 任務的 CopySpec 介面提供

CopySpec 有幾個額外的方法,可讓您控制複製過程,但這兩個方法是唯一需要的。into() 很簡單,需要一個目錄路徑作為其引數,其形式可以是 Project.file(java.lang.Object) 方法支援的任何形式。from() 配置要靈活得多。

from() 不僅接受多個引數,而且還允許使用幾種不同類型的引數。例如,一些最常見的類型是

  • String — 被視為檔案路徑,或者如果它以 "file://" 開頭,則被視為檔案 URI

  • File — 用作檔案路徑

  • FileCollectionFileTree — 集合中的所有檔案都包含在複製中

  • 任務 — 形成任務定義的輸出的檔案或目錄包含在內

實際上,from() 接受與 Project.files(java.lang.Object…​) 相同的所有引數,因此請參閱該方法以取得更詳細的可接受類型清單。

另一個需要考慮的事項是檔案路徑指的是哪種類型的東西

  • 檔案 — 檔案按原樣複製

  • 目錄 — 這實際上被視為檔案樹狀結構:其中的所有內容,包括子目錄,都會被複製。但是,目錄本身不包含在複製中。

  • 不存在的檔案 — 路徑被忽略

以下範例示範如何使用多個 from() 規格,每個規格都帶有不同的引數類型。您可能也會注意到 into() 是使用閉包 (在 Groovy 中) 或 Provider (在 Kotlin 中) 延遲配置的 — 這項技術也適用於 from()

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

請注意,into() 的延遲配置與子規格不同,即使語法相似。請留意引數的數量以區分它們。

在您自己的任務中複製檔案

如此處所述,在執行時期使用 Project.copy 方法與組態快取不相容。一個可能的解決方案是以適當的類別實作任務,並改用 FileSystemOperations.copy 方法,如組態快取章節中所述。

有時,您希望將檔案或目錄複製為任務的一部分。例如,基於不受支援的封存格式的自訂封存任務可能希望在封存之前將檔案複製到暫存目錄。您仍然希望利用 Gradle 的複製 API,而無需引入額外的 Copy 任務。

解決方案是使用 Project.copy(org.gradle.api.Action) 方法。使用複製規格配置它,其運作方式與 Copy 任務類似。以下是一個簡單的範例:

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

以上範例示範了基本語法,並突顯了使用 copy() 方法的兩個主要限制:

  1. copy() 方法不是增量式的。範例中的 copyMethod 任務將總是執行,因為它沒有關於哪些檔案構成任務輸入的資訊。您必須手動定義任務的輸入和輸出。

  2. 將任務作為複製來源使用,即作為 from() 的引數,不會在您的任務和該複製來源之間建立自動任務依賴關係。因此,如果您將 copy() 方法用作任務動作的一部分,您必須明確宣告所有輸入和輸出,才能獲得正確的行為。

以下範例示範如何使用 任務輸入和輸出的動態 API 來解決這些限制:

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

由於其內建支援增量式建置和任務依賴關係推斷,這些限制使得在任何可能的情況下,最好都使用 Copy 任務。這就是為什麼 copy() 方法旨在供需要複製檔案作為其功能一部分的自訂任務使用。使用 copy() 方法的自訂任務應宣告與複製動作相關的必要輸入和輸出。

重新命名檔案

可以使用 CopySpec API 在 Gradle 中重新命名檔案,該 API 提供了在複製檔案時重新命名檔案的方法。

使用 Copy.rename()

如果您的建置所使用和產生的檔案有時名稱不合適,您可以在複製這些檔案時重新命名它們。Gradle 允許您使用 rename() 配置將此作為複製規格的一部分來完成。

以下範例從任何具有 "-staging" 標記的檔案名稱中移除該標記:

build.gradle.kts
tasks.register<Copy>("copyFromStaging") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWar"))

    rename("(.+)-staging(.+)", "$1$2")
}
build.gradle
tasks.register('copyFromStaging', Copy) {
    from "src/main/webapp"
    into layout.buildDirectory.dir('explodedWar')

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

如以上範例所示,您可以對此使用正則表達式或閉包,這些閉包使用更複雜的邏輯來決定目標檔案名稱。例如,以下任務截斷檔案名稱:

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

與篩選一樣,您也可以透過將其配置為 from() 上子規格的一部分來重新命名檔案的子集。

使用 Copyspec.rename{}

關於如何在複製時重新命名檔案的範例為您提供了執行此操作所需的大部分資訊。它示範了重新命名的兩個選項:

  1. 使用正則表達式

  2. 使用閉包

正則表達式是一種靈活的重新命名方法,特別是因為 Gradle 支援正則表達式群組,讓您可以移除和取代來源檔案名稱的部分。以下範例示範如何使用簡單的正則表達式,從任何包含字串 "-staging" 的檔案名稱中移除該字串:

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

您可以使用 Java Pattern 類別和取代字串支援的任何正則表達式。rename() 的第二個引數的運作原理與 Matcher.appendReplacement() 方法相同。

Groovy 建置腳本中的正則表達式

人們在此環境中使用正則表達式時,通常會遇到兩個常見問題:

  1. 如果您對第一個引數使用 slashy 字串 (以 '/' 分隔的字串),您必須包含 rename() 的括號,如以上範例所示。

  2. 對於第二個引數,最好使用單引號,否則您需要逸出群組取代中的 '$',即 "\$1\$2"

第一個問題只是小小的麻煩,但 slashy 字串的優點是您不必逸出正則表達式中的反斜線 ('\') 字元。第二個問題源於 Groovy 對雙引號和 slashy 字串中使用 ${ } 語法嵌入式表達式的支援。

rename() 的閉包語法很簡單明瞭,可用於簡單正則表達式無法處理的任何需求。您會獲得檔案的名稱,如果您不想變更名稱,則傳回該檔案的新名稱或 null。請注意,閉包將針對複製的每個檔案執行,因此請盡可能避免昂貴的操作。

篩選檔案

在 Gradle 中篩選檔案涉及根據特定條件選擇性地包含或排除檔案。

使用 CopySpec.include()CopySpec.exclude()

您可以透過 CopySpec.include(java.lang.String…)CopySpec.exclude(java.lang.String…) 方法在任何複製規格中套用篩選。

這些方法通常與 Ant 樣式的包含或排除模式一起使用,如 PatternFilterable 中所述。

您也可以執行更複雜的邏輯,方法是使用一個閉包,該閉包接受 FileTreeElement,如果應包含該檔案,則傳回 true,否則傳回 false。以下範例示範了兩種形式,確保僅複製 .html.jsp 檔案,但內容中包含 "DRAFT" 字詞的 .html 檔案除外:

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

您在這個時候可能會問自己一個問題,當包含和排除模式重疊時會發生什麼事?哪個模式獲勝?以下是基本規則:

  • 如果沒有明確的包含或排除,則包含所有內容。

  • 如果指定了至少一個包含,則僅包含符合模式的檔案和目錄。

  • 任何排除模式都會覆寫任何包含,因此,如果檔案或目錄符合至少一個排除模式,則無論包含模式如何,都不會包含它。

在建立組合的包含和排除規格時,請記住這些規則,以便您最終獲得您想要的確切行為。

請注意,以上範例中的包含和排除將套用於所有 from() 配置。如果您想要將篩選套用於複製檔案的子集,您需要使用子規格

篩選檔案內容

在 Gradle 中篩選檔案內容涉及將檔案中的佔位符或符記取代為動態值。

使用 CopySpec.filter()

在複製檔案時轉換檔案內容涉及基本範本,該範本使用符記取代、移除文字行,甚至使用功能齊全的範本引擎進行更複雜的篩選。

以下範例示範了幾種形式的篩選,包括使用 CopySpec.expand(java.util.Map) 方法的符記取代,以及另一個搭配 CopySpec.filter(java.lang.Class)Ant 篩選器的使用:

build.gradle.kts
import org.apache.tools.ant.filters.FixCrLfFilter
import org.apache.tools.ant.filters.ReplaceTokens
tasks.register<Copy>("filter") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWar"))
    // Substitute property tokens in files
    expand("copyright" to "2009", "version" to "2.3.1")
    // Use some of the filters provided by Ant
    filter(FixCrLfFilter::class)
    filter(ReplaceTokens::class, "tokens" to mapOf("copyright" to "2009", "version" to "2.3.1"))
    // Use a closure to filter each line
    filter { line: String ->
        "[$line]"
    }
    // Use a closure to remove lines
    filter { line: String ->
        if (line.startsWith('-')) null else line
    }
    filteringCharset = "UTF-8"
}
build.gradle
import org.apache.tools.ant.filters.FixCrLfFilter
import org.apache.tools.ant.filters.ReplaceTokens

tasks.register('filter', Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWar')
    // Substitute property tokens in files
    expand(copyright: '2009', version: '2.3.1')
    // Use some of the filters provided by Ant
    filter(FixCrLfFilter)
    filter(ReplaceTokens, tokens: [copyright: '2009', version: '2.3.1'])
    // Use a closure to filter each line
    filter { String line ->
        "[$line]"
    }
    // Use a closure to remove lines
    filter { String line ->
        line.startsWith('-') ? null : line
    }
    filteringCharset = 'UTF-8'
}

filter() 方法有兩種變體,它們的行為有所不同:

  • 一種接受 FilterReader,旨在與 Ant 篩選器 (例如 ReplaceTokens) 搭配使用。

  • 另一種接受閉包或 Transformer,為來源檔案的每一行定義轉換。

請注意,這兩種變體都假設來源檔案是以文字為基礎的。當您將 ReplaceTokens 類別與 filter() 搭配使用時,您會建立一個範本引擎,該引擎會將 @tokenName@ 形式的符記 (Ant 樣式的符記) 取代為您定義的值。

使用 CopySpec.expand()

expand() 方法將來源檔案視為 Groovy 範本,它會評估和展開 ${expression} 形式的表達式。

您可以傳入屬性名稱和值,然後在來源檔案中展開這些名稱和值。expand() 允許的不僅僅是基本的符記取代,因為嵌入式表達式是功能齊全的 Groovy 表達式。

指定讀取和寫入檔案時的字元集是一種良好的做法。否則,轉換對於非 ASCII 文字將無法正常運作。您可以使用 CopySpec.setFilteringCharset(String) 屬性配置字元集。如果未指定,則會使用 JVM 預設字元集,這可能與您想要的字元集不同。

設定檔案權限

在 Gradle 中設定檔案權限涉及指定在建置過程中建立或修改的檔案或目錄的權限。

使用 CopySpec.filePermissions{}

對於任何參與複製檔案的 CopySpec,無論是 Copy 任務本身,還是任何子規格,您都可以透過 CopySpec.filePermissions {} 配置區塊明確設定目標檔案將擁有的權限。

使用 CopySpec.dirPermissions{}

您也可以對目錄執行相同的操作,與檔案無關,透過 CopySpec.dirPermissions {} 配置區塊。

不明確設定權限將保留原始檔案或目錄的權限。
build.gradle.kts
tasks.register<Copy>("permissions") {
    from("src/main/webapp")
    into(layout.buildDirectory.dir("explodedWar"))
    filePermissions {
        user {
            read = true
            execute = true
        }
        other.execute = false
    }
    dirPermissions {
        unix("r-xr-x---")
    }
}
build.gradle
tasks.register('permissions', Copy) {
    from 'src/main/webapp'
    into layout.buildDirectory.dir('explodedWar')
    filePermissions {
        user {
            read = true
            execute = true
        }
        other.execute = false
    }
    dirPermissions {
        unix('r-xr-x---')
    }
}

有關檔案權限的詳細說明,請參閱 FilePermissionsUserClassFilePermissions。有關範例中使用的便利方法的詳細資訊,請參閱 ConfigurableFilePermissions.unix(String)

對檔案或目錄權限使用空配置區塊仍然會明確設定它們,只是設定為固定的預設值。這些配置區塊中的所有內容都相對於預設值。檔案和目錄的預設權限不同:

  • 檔案擁有者的讀取和寫入權限,群組的讀取權限,其他的讀取權限 (0644, rw-r—​r--)

  • 目錄擁有者的讀取、寫入和執行權限,群組的讀取和執行權限,其他的讀取和執行權限 (0755, rwxr-xr-x)

移動檔案和目錄

在 Gradle 中移動檔案和目錄是一個簡單明瞭的過程,可以使用多個 API 來完成。在您的建置腳本中實作檔案移動邏輯時,請務必考慮檔案路徑、衝突和任務依賴關係。

使用 File.renameTo()

File.renameTo() 是 Java 中的一種方法 (依此類推,在 Gradle 的 Groovy DSL 中),用於重新命名或移動檔案或目錄。當您在 File 物件上呼叫 renameTo() 時,您會提供另一個 File 物件,表示新的名稱或位置。如果操作成功,renameTo() 會傳回 true;否則,會傳回 false

務必注意,renameTo() 有一些限制和平台特定的行為。

在此範例中,moveFile 任務使用 Copy 任務類型來指定來源和目標目錄。在 doLast 閉包內,它使用 File.renameTo() 將檔案從來源目錄移動到目標目錄:

task moveFile {
    doLast {
        def sourceFile = file('source.txt')
        def destFile = file('destination/new_name.txt')

        if (sourceFile.renameTo(destFile)) {
            println "File moved successfully."
        }
    }
}

使用 Copy 任務

在此範例中,moveFile 任務將檔案 source.txt 複製到目標目錄,並在此過程中將其重新命名為 new_name.txt。這達到了與移動檔案類似的效果。

task moveFile(type: Copy) {
    from 'source.txt'
    into 'destination'
    rename { fileName ->
        'new_name.txt'
    }
}

刪除檔案和目錄

在 Gradle 中刪除檔案和目錄涉及從檔案系統中移除它們。

使用 Delete 任務

您可以使用 Delete 任務輕鬆刪除檔案和目錄。您必須以 Project.files(java.lang.Object…) 方法支援的方式指定要刪除的檔案和目錄。

例如,以下任務刪除建置輸出目錄的整個內容:

build.gradle.kts
tasks.register<Delete>("myClean") {
    delete(buildDir)
}
build.gradle
tasks.register('myClean', Delete) {
    delete buildDir
}

如果您想要更精確地控制要刪除哪些檔案,您不能使用包含和排除,就像您對複製檔案使用它們的方式一樣。相反,您可以使用 FileCollectionFileTree 的內建篩選機制。以下範例正是這樣做,以清除來源目錄中的暫存檔:

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

使用 Project.delete()

Project.delete(org.gradle.api.Action) 方法可以刪除檔案和目錄。

此方法接受一個或多個引數,表示要刪除的檔案或目錄。

例如,以下任務刪除建置輸出目錄的整個內容:

build.gradle.kts
tasks.register<Delete>("myClean") {
    delete(buildDir)
}
build.gradle
tasks.register('myClean', Delete) {
    delete buildDir
}

如果您想要更精確地控制要刪除哪些檔案,您不能使用包含和排除,就像您對複製檔案使用它們的方式一樣。相反,您可以使用 FileCollectionFileTree 的內建篩選機制。以下範例正是這樣做,以清除來源目錄中的暫存檔:

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

建立封存檔

從 Gradle 的角度來看,將檔案封裝到封存檔中實際上是一種複製,其中目標是封存檔,而不是檔案系統上的目錄。建立封存檔看起來很像複製,具有所有相同的功能。

使用 ZipTarJar 任務

最簡單的情況涉及封存目錄的整個內容,此範例透過建立 toArchive 目錄的 ZIP 檔來示範:

build.gradle.kts
tasks.register<Zip>("packageDistribution") {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = layout.buildDirectory.dir("dist")

    from(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('packageDistribution', Zip) {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = layout.buildDirectory.dir('dist')

    from layout.buildDirectory.dir("toArchive")
}

請注意,我們如何指定封存檔的目標和名稱,而不是 into():兩者都是必要的。您通常不會看到它們被明確設定,因為大多數專案都會套用 Base Plugin。它為這些屬性提供了一些慣例值。

以下範例示範了這一點;您可以在封存檔命名章節中了解有關慣例的更多資訊。

每種封存檔類型都有其自己的任務類型,最常見的是 ZipTarJar。它們都共用 Copy 的大多數配置選項,包括篩選和重新命名。

最常見的案例之一涉及將檔案複製到指定的封存檔子目錄中。例如,假設您想要將所有 PDF 封裝到封存檔根目錄中的 docs 目錄中。此 docs 目錄在來源位置中不存在,因此您必須將其建立為封存檔的一部分。您可以透過僅針對 PDF 新增 into() 宣告來執行此操作:

build.gradle.kts
plugins {
    base
}

version = "1.0.0"

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

    from(layout.buildDirectory.dir("toArchive")) {
        include("**/*.pdf")
        into("docs")
    }
}
build.gradle
plugins {
    id 'base'
}

version = "1.0.0"

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

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

如您所見,您可以在複製規格中有多個 from() 宣告,每個宣告都有自己的配置。請參閱 使用子複製規格,以取得有關此功能的更多資訊。

了解封存檔建立

封存檔本質上是獨立的檔案系統,而 Gradle 將它們視為如此。這就是為什麼使用封存檔類似於使用檔案和目錄。

Gradle 開箱即用地支援建立 ZIP 和 TAR 封存檔,以及 Java 的 JAR、WAR 和 EAR 格式 — Java 的封存檔格式都是 ZIP 檔。這些格式中的每一種都有對應的任務類型來建立它們:ZipTarJarWarEar。這些都以相同的方式運作,並且基於複製規格,就像 Copy 任務一樣。

建立封存檔本質上是一種檔案複製,其中目標是隱含的,即封存檔本身。以下是一個基本範例,指定了目標封存檔的路徑和名稱:

build.gradle.kts
tasks.register<Zip>("packageDistribution") {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = layout.buildDirectory.dir("dist")

    from(layout.buildDirectory.dir("toArchive"))
}
build.gradle
tasks.register('packageDistribution', Zip) {
    archiveFileName = "my-distribution.zip"
    destinationDirectory = layout.buildDirectory.dir('dist')

    from layout.buildDirectory.dir("toArchive")
}

當您建立封存檔時,可以使用複製規格的全部功能,這表示您可以執行內容篩選、檔案重新命名或上一節中涵蓋的任何其他操作。常見的需求是將檔案複製到封存檔的子目錄中,這些子目錄在來源資料夾中不存在,可以使用 into() 子規格來實現。

Gradle 允許您建立任意數量的封存檔任務,但值得考慮的是,許多基於慣例的外掛程式都提供了自己的封存檔任務。例如,Java 外掛程式新增了一個 jar 任務,用於將專案的已編譯類別和資源封裝在 JAR 中。許多這些外掛程式為封存檔的名稱和使用的複製規格提供了合理的慣例。我們建議您盡可能使用這些任務,而不是用您自己的任務覆寫它們。

命名封存檔

Gradle 對於封存檔的命名以及根據您的專案使用的外掛程式在何處建立它們,有幾個慣例。Base Plugin 提供了主要的慣例,它預設在 layout.buildDirectory.dir("distributions") 目錄中建立封存檔,並且通常使用 [專案名稱]-[版本].[類型] 形式的封存檔名稱。

以下範例來自名為 archive-naming 的專案,因此 myZip 任務會建立名為 archive-naming-1.0.zip 的封存檔:

build.gradle.kts
plugins {
    base
}

version = "1.0"

tasks.register<Zip>("myZip") {
    from("somedir")
    val projectDir = layout.projectDirectory.asFile
    doLast {
        println(archiveFileName.get())
        println(destinationDirectory.get().asFile.relativeTo(projectDir))
        println(archiveFile.get().asFile.relativeTo(projectDir))
    }
}
build.gradle
plugins {
    id 'base'
}

version = 1.0

tasks.register('myZip', Zip) {
    from 'somedir'
    File projectDir = layout.projectDirectory.asFile
    doLast {
        println archiveFileName.get()
        println projectDir.relativePath(destinationDirectory.get().asFile)
        println projectDir.relativePath(archiveFile.get().asFile)
    }
}
$ gradle -q myZip
archive-naming-1.0.zip
build/distributions
build/distributions/archive-naming-1.0.zip

請注意,封存檔名稱不是從建立它的任務名稱衍生而來的。

如果您想要變更產生的封存檔的名稱和位置,您可以為對應任務的 archiveFileNamedestinationDirectory 屬性提供值。這些會覆寫任何其他套用的慣例。

或者,您可以使用 AbstractArchiveTask.getArchiveFileName() 提供的預設封存檔名稱模式:[archiveBaseName]-[archiveAppendix]-[archiveVersion]-[archiveClassifier].[archiveExtension]。您可以分別在任務上設定這些屬性中的每一個。請注意,Base Plugin 使用專案名稱作為 archiveBaseName、專案版本作為 archiveVersion 以及封存檔類型作為 archiveExtension 的慣例。它不為其他屬性提供值。

此範例 — 與上面的範例來自同一個專案 — 僅配置了 archiveBaseName 屬性,覆寫了專案名稱的預設值:

build.gradle.kts
tasks.register<Zip>("myCustomZip") {
    archiveBaseName = "customName"
    from("somedir")

    doLast {
        println(archiveFileName.get())
    }
}
build.gradle
tasks.register('myCustomZip', Zip) {
    archiveBaseName = 'customName'
    from 'somedir'

    doLast {
        println archiveFileName.get()
    }
}
$ gradle -q myCustomZip
customName-1.0.zip

您也可以透過使用專案屬性 archivesBaseName,覆寫建置中所有封存檔任務的預設 archiveBaseName 值,如下列範例所示:

build.gradle.kts
plugins {
    base
}

version = "1.0"

base {
    archivesName = "gradle"
    distsDirectory = layout.buildDirectory.dir("custom-dist")
    libsDirectory = layout.buildDirectory.dir("custom-libs")
}

val myZip by tasks.registering(Zip::class) {
    from("somedir")
}

val myOtherZip by tasks.registering(Zip::class) {
    archiveAppendix = "wrapper"
    archiveClassifier = "src"
    from("somedir")
}

tasks.register("echoNames") {
    val projectNameString = project.name
    val archiveFileName = myZip.flatMap { it.archiveFileName }
    val myOtherArchiveFileName = myOtherZip.flatMap { it.archiveFileName }
    doLast {
        println("Project name: $projectNameString")
        println(archiveFileName.get())
        println(myOtherArchiveFileName.get())
    }
}
build.gradle
plugins {
    id 'base'
}

version = 1.0
base {
    archivesName = "gradle"
    distsDirectory = layout.buildDirectory.dir('custom-dist')
    libsDirectory = layout.buildDirectory.dir('custom-libs')
}

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

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

tasks.register('echoNames') {
    def projectNameString = project.name
    def archiveFileName = myZip.flatMap { it.archiveFileName }
    def myOtherArchiveFileName = myOtherZip.flatMap { it.archiveFileName }
    doLast {
        println "Project name: $projectNameString"
        println archiveFileName.get()
        println myOtherArchiveFileName.get()
    }
}
$ gradle -q echoNames
Project name: archives-changed-base-name
gradle-1.0.zip
gradle-wrapper-1.0-src.zip

您可以在 AbstractArchiveTask 的 API 文件中找到所有可能的封存檔任務屬性。不過,我們也在此處總結了主要的屬性:

archiveFileNameProperty<String>,預設值:archiveBaseName-archiveAppendix-archiveVersion-archiveClassifier.archiveExtension

產生的封存檔的完整檔案名稱。如果預設值中的任何屬性為空,則會捨棄其 '-' 分隔符號。

archiveFileProvider<RegularFile>唯讀,預設值:destinationDirectory/archiveFileName

產生的封存檔的絕對檔案路徑。

destinationDirectoryDirectoryProperty,預設值:取決於封存檔類型

放置產生的封存檔的目標目錄。預設情況下,JAR 和 WAR 會進入 layout.buildDirectory.dir("libs")。ZIP 和 TAR 會進入 layout.buildDirectory.dir("distributions")

archiveBaseNameProperty<String>,預設值:project.name

封存檔檔案名稱的基礎名稱部分,通常是專案名稱或對其內容的一些其他描述性名稱。

archiveAppendixProperty<String>,預設值:null

緊接在基礎名稱之後的封存檔檔案名稱的附錄部分。它通常用於區分不同形式的內容,例如程式碼和文件,或最小發行版與完整或完整發行版。

archiveVersionProperty<String>,預設值:project.version

封存檔檔案名稱的版本部分,通常採用正常專案或產品版本的形式。

archiveClassifierProperty<String>,預設值:null

封存檔檔案名稱的分類器部分。通常用於區分以不同平台為目標的封存檔。

archiveExtensionProperty<String>,預設值:取決於封存檔類型和壓縮類型

封存檔的檔案名稱副檔名。預設情況下,這是根據封存檔任務類型和壓縮類型 (如果您要建立 TAR) 設定的。將是以下其中之一:zipjarwartartgztbz2。如果您願意,當然可以將其設定為自訂副檔名。

將封存檔作為檔案樹狀結構使用

封存檔是一個目錄和檔案階層,封裝在單一檔案中。換句話說,它是檔案樹狀結構的特殊情況,而這正是 Gradle 處理封存檔的方式。

您可以使用 Project.zipTree(java.lang.Object)Project.tarTree(java.lang.Object) 方法,而不是僅適用於正常檔案系統的 fileTree() 方法,來包裝相應類型的封存檔 (請注意,JAR、WAR 和 EAR 檔案是 ZIP 檔)。這兩種方法都會傳回 FileTree 實例,然後您可以像使用正常檔案樹狀結構一樣使用它們。例如,您可以透過將封存檔的內容複製到檔案系統上的某些目錄來解壓縮封存檔的部分或全部檔案。或者,您可以將一個封存檔合併到另一個封存檔中。

以下是一些建立基於封存檔的檔案樹狀結構的簡單範例:

build.gradle.kts
// Create a ZIP file tree using path
val zip: FileTree = zipTree("someFile.zip")

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

// tar tree attempts to guess the compression based on the file extension
// however if you must specify the compression explicitly you can:
val someTar: FileTree = tarTree(resources.gzip("someTar.ext"))
build.gradle
// Create a ZIP file tree using path
FileTree zip = zipTree('someFile.zip')

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

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

您可以在下面的解壓縮封存檔章節中看到解壓縮封存檔的實用範例。

使用 AbstractArchiveTask 進行可重現的建置

有時,希望在不同的機器上完全相同地重新建立封存檔,位元組接位元組。您想要確保從原始程式碼建置成品會產生相同的結果,無論何時何地建置。這對於像 reproducible-builds.org 這樣的專案是必要的。

重現相同的位元組對位元組封存檔帶來了一些挑戰,因為封存檔中檔案的順序受到基礎檔案系統的影響。每次從原始程式碼建置 ZIP、TAR、JAR、WAR 或 EAR 時,封存檔內檔案的順序可能會變更。僅具有不同時間戳記的檔案也會導致從建置到建置的封存檔有所不同。

Gradle 隨附的所有 AbstractArchiveTask (例如 Jar、Zip) 任務都包含對產生可重現封存檔的支援。

例如,若要使 Zip 任務可重現,您需要將 Zip.isReproducibleFileOrder() 設定為 true,並將 Zip.isPreserveFileTimestamps() 設定為 false。為了使建置中的所有封存檔任務都可重現,請考慮將以下配置新增至您的建置檔案:

build.gradle.kts
tasks.withType<AbstractArchiveTask>().configureEach {
    isPreserveFileTimestamps = false
    isReproducibleFileOrder = true
}
build.gradle
tasks.withType(AbstractArchiveTask).configureEach {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}

通常,您會想要發佈封存檔,以便它可以從另一個專案中使用。

解壓縮封存檔

封存檔實際上是獨立的檔案系統,因此解壓縮它們是將檔案從該檔案系統複製到本機檔案系統 — 甚至複製到另一個封存檔中。Gradle 透過提供一些包裝函式來啟用此功能,這些函式使封存檔可以作為檔案的階層式集合 (檔案樹狀結構) 使用。

使用 Project.zipTreeProject.tarTree

感興趣的兩個函式是 Project.zipTree(java.lang.Object)Project.tarTree(java.lang.Object),它們從對應的封存檔產生 FileTree

然後,可以在 from() 規格中使用該檔案樹狀結構,如下所示:

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

與正常複製一樣,您可以透過篩選器控制解壓縮哪些檔案,甚至可以在解壓縮時重新命名檔案

更進階的處理可以由 eachFile() 方法處理。例如,您可能需要將封存檔的不同子樹解壓縮到目標目錄中的不同路徑。以下範例使用該方法將封存檔 libs 目錄中的檔案解壓縮到根目標目錄中,而不是解壓縮到 libs 子目錄中:

build.gradle.kts
tasks.register<Copy>("unpackLibsDirectory") {
    from(zipTree("src/resources/thirdPartyResources.zip")) {
        include("libs/**")  (1)
        eachFile {
            relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())  (2)
        }
        includeEmptyDirs = false  (3)
    }
    into(layout.buildDirectory.dir("resources"))
}
build.gradle
tasks.register('unpackLibsDirectory', Copy) {
    from(zipTree("src/resources/thirdPartyResources.zip")) {
        include "libs/**"  (1)
        eachFile { fcd ->
            fcd.relativePath = new RelativePath(true, fcd.relativePath.segments.drop(1))  (2)
        }
        includeEmptyDirs = false  (3)
    }
    into layout.buildDirectory.dir("resources")
}
1 僅解壓縮位於 libs 目錄中的檔案子集
2 透過從檔案路徑中捨棄 libs 段,將解壓縮檔案的路徑重新對應到目標目錄中
3 忽略重新對應產生的空目錄,請參閱下面的「注意」註解

您無法使用此技術變更空目錄的目標路徑。您可以在 此問題中了解更多資訊。

如果您是 Java 開發人員,想知道為什麼沒有 jarTree() 方法,那是因為 zipTree() 非常適用於 JAR、WAR 和 EAR。

建立 "uber" 或 "fat" JAR

在 Java 中,應用程式及其依賴項通常封裝為單一發行封存檔中的個別 JAR。這種情況仍然發生,但現在另一種常見的方法是將依賴項的類別和資源直接放置到應用程式 JAR 中,建立所謂的 Uber 或 fat JAR。

在 Gradle 中建立 "uber" 或 "fat" JAR 涉及將所有依賴項封裝到單一 JAR 檔案中,使其更易於發佈和執行應用程式。

使用 Shadow 外掛程式

Gradle 沒有完全內建對建立 uber JAR 的支援,但您可以使用第三方外掛程式,例如 Shadow 外掛程式 (com.gradleup.shadow) 來實現此目的。此外掛程式將您的專案類別和依賴項封裝到單一 JAR 檔案中。

使用 Project.zipTree()Jar 任務

若要將其他 JAR 檔案的內容複製到應用程式 JAR 中,請使用 Project.zipTree(java.lang.Object) 方法和 Jar 任務。以下範例中的 uberJar 任務示範了這一點:

build.gradle.kts
plugins {
    java
}

version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation("commons-io:commons-io:2.6")
}

tasks.register<Jar>("uberJar") {
    archiveClassifier = "uber"

    from(sourceSets.main.get().output)

    dependsOn(configurations.runtimeClasspath)
    from({
        configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
    })
}
build.gradle
plugins {
    id 'java'
}

version = '1.0.0'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'commons-io:commons-io:2.6'
}

tasks.register('uberJar', Jar) {
    archiveClassifier = 'uber'

    from sourceSets.main.output

    dependsOn configurations.runtimeClasspath
    from {
        configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
    }
}

在此案例中,我們採用專案的執行時期依賴項 — configurations.runtimeClasspath.files — 並使用 zipTree() 方法包裝每個 JAR 檔案。結果是一個 ZIP 檔案樹狀結構的集合,其內容與應用程式類別一起複製到 uber JAR 中。

建立目錄

許多任務需要建立目錄來儲存它們產生的檔案,這就是為什麼 Gradle 在任務明確定義檔案和目錄輸出時,自動管理任務的這個方面。所有核心 Gradle 任務都確保它們需要的任何輸出目錄都會被建立 (如有必要),使用此機制。

使用 File.mkdirsFiles.createDirectories

在您需要手動建立目錄的情況下,您可以使用標準的 Files.createDirectoriesFile.mkdirs 方法,從您的建置腳本或自訂任務實作中。

以下是一個簡單的範例,在專案資料夾中建立單一 images 目錄:

build.gradle.kts
tasks.register("ensureDirectory") {
    // Store target directory into a variable to avoid project reference in the configuration cache
    val directory = file("images")

    doLast {
        Files.createDirectories(directory.toPath())
    }
}
build.gradle
tasks.register('ensureDirectory') {
    // Store target directory into a variable to avoid project reference in the configuration cache
    def directory = file("images")

    doLast {
        Files.createDirectories(directory.toPath())
    }
}

Apache Ant 手冊中所述,mkdir 任務會自動在給定路徑中建立所有必要的目錄。如果目錄已存在,它將不會執行任何操作。

使用 Project.mkdir

您可以使用 mkdir 方法在 Gradle 中建立目錄,該方法在 Project 物件中可用。此方法接受 File 物件或 String,表示要建立的目錄的路徑:

tasks.register('createDirs') {
    doLast {
        mkdir 'src/main/resources'
        mkdir file('build/generated')

        // Create multiple dirs
        mkdir files(['src/main/resources', 'src/test/resources'])

        // Check dir existence
        def dir = file('src/main/resources')
        if (!dir.exists()) {
            mkdir dir
        }
    }
}

安裝可執行檔

當您建置獨立可執行檔時,您可能想要將此檔案安裝在您的系統上,使其最終位於您的路徑中。

使用 Copy 任務

您可以使用 Copy 工作來將可執行檔安裝到共用目錄中,例如 /usr/local/bin。安裝目錄可能包含許多其他可執行檔,其中有些甚至可能 Gradle 無法讀取。為了支援 Copy 工作目標目錄中無法讀取的檔案,並避免耗時的最新檢查,您可以使用 Task.doNotTrackState()

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

將單一檔案部署到應用程式伺服器

將單一檔案部署到應用程式伺服器通常指的是將已封裝的應用程式產生物件 (例如 WAR 檔案) 傳輸到應用程式伺服器的部署目錄的過程。

使用 Copy 工作

當使用應用程式伺服器時,您可以使用 Copy 工作來部署應用程式封存檔 (例如 WAR 檔案)。由於您部署的是單一檔案,因此 Copy 的目標目錄是整個部署目錄。部署目錄有時確實包含無法讀取的檔案,例如具名管道,因此 Gradle 在執行最新檢查時可能會遇到問題。為了支援這種使用案例,您可以使用 Task.doNotTrackState()

build.gradle.kts
plugins {
    war
}

tasks.register<Copy>("deployToTomcat") {
    from(tasks.war)
    into(layout.projectDirectory.dir("tomcat/webapps"))
    doNotTrackState("Deployment directory contains unreadable files")
}
build.gradle
plugins {
    id 'war'
}

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