幾乎每個 Gradle 建置都會以某種方式與檔案互動:例如原始碼檔案、檔案相依性、報告等。這就是為什麼 Gradle 附帶一個全面的 API,讓您可以輕鬆執行所需的檔案操作。

API 有兩個部分

  • 指定要處理哪些檔案和目錄

  • 指定要如何處理它們

深入探討檔案路徑 區段詳細介紹了第一個部分,而後續區段,例如 深入探討檔案複製,則介紹了第二個部分。首先,我們將向您展示使用者會遇到的最常見場景範例。

複製單一檔案

您可以透過建立 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")
}

ProjectLayout 類別用於尋找相對於目前專案的檔案或目錄路徑。這是讓建置指令碼不論專案路徑為何都能運作的常見方式。然後使用檔案和目錄路徑來指定要使用 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
圖 1. 平面篩選器對複製的影響

你可以使用 Ant 樣式的全域模式 (**/*) 來包含子目錄中的檔案,如這個更新的範例所示

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
圖 2. 深度篩選器對複製的影響

要注意的一件事是,像這樣的深度篩選器具有複製 reports 下目錄結構以及檔案的副作用。如果你只想複製檔案而不複製目錄結構,你需要使用明確的 fileTree(dir) { includes }.files 表達式。我們會在 檔案樹 區段中進一步說明檔案樹和檔案集合之間的差異。

這只是你在處理 Gradle 建置中的檔案操作時可能會遇到的行為變化之一。幸運的是,Gradle 幾乎為所有這些使用案例提供了優雅的解決方案。請閱讀本章後面的深入區段,以進一步了解 Gradle 中的檔案操作如何運作,以及你有哪些選項可以設定它們。

複製目錄階層

你可能需要複製檔案,以及它們所在的目錄結構。這是當你指定目錄為 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(),而前一區段中的指令適用於整個任務。複製規範中這些不同的詳細程度層級,讓你能夠輕鬆處理你會遇到的大多數需求。你可以在 子規範 區段中進一步了解這一點。

建立封存檔 (zip、tar 等)

從 Gradle 的角度來看,將檔案打包成封存檔實際上是一種複製,其中目的地是封存檔檔案,而不是檔案系統上的目錄。這表示建立封存檔看起來很像複製,並具有所有相同的功能!

最簡單的情況是封存目錄的全部內容,此範例示範了這一點,它建立了 toArchive 目錄的 ZIP

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

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

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

請注意,我們指定的是檔案的目的地和名稱,而不是 into():兩者都是必要的。您通常不會看到它們被明確設定,因為大多數專案都會套用 基本外掛。它會提供這些屬性的某些慣例值。下一個範例示範了這一點,您可以在 檔案命名 區段中進一步了解這些慣例。

每種類型的檔案都有其自己的工作類型,最常見的類型為 ZipTarJar。它們都共用大部分 Copy 的組態選項,包括篩選和重新命名。

最常見的場景之一包括將檔案複製到檔案指定子目錄中。例如,假設您想要將所有 PDF 封裝到檔案根目錄的 docs 目錄中。這個 docs 目錄不存在於來源位置,因此您必須將它建立為檔案的一部分。您可以透過只針對 PDF 加入 into() 宣告來執行此動作

build.gradle.kts
plugins {
    base
}

version = "1.0.0"

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

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

version = "1.0.0"

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

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

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

解壓縮檔案

檔案實際上是自給自足的檔案系統,因此解壓縮它們就是將檔案從該檔案系統複製到本機檔案系統,甚至複製到另一個檔案中。Gradle 提供了一些包裝函式,讓檔案可用作檔案的階層式集合(檔案樹),進而讓 Gradle 能夠執行此動作。

兩個有用的函式為 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 使得這個方法容易達成。考慮目標:將其他 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 工作確保它們需要的任何輸出目錄在必要時使用這個機制建立。

在您需要手動建立目錄的情況下,您可以從您的建置指令碼或自訂工作實作中使用標準的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工作會自動在給定的路徑中建立所有必要的目錄,如果目錄已存在,則不執行任何動作。

移動檔案和目錄

Gradle 沒有用於移動檔案和目錄的 API,但您可以使用Apache Ant 整合來輕鬆執行此動作,如以下範例所示

build.gradle.kts
tasks.register("moveReports") {
    // Store the build directory into a variable to avoid project reference in the configuration cache
    val dir = buildDir

    doLast {
        ant.withGroovyBuilder {
            "move"("file" to "${dir}/reports", "todir" to "${dir}/toArchive")
        }
    }
}
build.gradle
tasks.register('moveReports') {
    // Store the build directory into a variable to avoid project reference in the configuration cache
    def dir = buildDir

    doLast {
        ant.move file: "${dir}/reports",
                 todir: "${dir}/toArchive"
    }
}

這不是常見的需求,應謹慎使用,因為您會遺失資訊,而且很容易中斷建置。一般來說,最好複製目錄和檔案。

複製時重新命名檔案

您的建置所使用和產生的檔案有時名稱不適合,在這種情況下,您會想要在複製檔案時重新命名它們。Gradle 允許您使用rename()設定檔將此動作作為複製規範的一部分。

以下範例會移除任何具有「-staging」標記的檔案名稱中的標記

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() 上的子規格中設定,將重新命名套用至檔案的子集。

刪除檔案和目錄

您可以使用 Delete 任務或 Project.delete(org.gradle.api.Action) 方法輕鬆刪除檔案和目錄。在這兩種情況下,您都可以在 Project.files(java.lang.Object…​) 方法支援的方式中,指定要刪除的檔案和目錄。

例如,下列任務會刪除組建輸出目錄中的所有內容

範例 17. 刪除目錄
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 建立在標準 Java File 類別上,它代表單一檔案的位置,並提供新的 API 來處理路徑集合。本節將向您展示如何使用 Gradle API 來指定檔案路徑,以用於任務和檔案操作。

但首先,有一個關於在您的組建中使用硬式編碼檔案路徑的重要注意事項。

關於硬式編碼檔案路徑

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

在可能的情況下,您應該使用任務、任務屬性和 專案屬性(依此順序優先)來設定檔案路徑。例如,如果您要建立一個打包 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 提供 Project.file(java.lang.Object) 方法,用於指定單一檔案或目錄的位置。相對路徑相對於專案目錄解析,而絕對路徑保持不變。

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

以下是一些使用 `file()` 方法搭配不同類型參數的範例

範例 20. 尋找檔案
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()` 方法會永遠將相對路徑轉換成相對於目前專案目錄的路徑,這可能是子專案。如果你想要使用相對於根專案目錄的路徑,則需要使用特殊的 Project.getRootDir() 屬性來建立絕對路徑,如下所示

build.gradle.kts
val configFile = file("$rootDir/shared/config.xml")
build.gradle
File configFile = file("$rootDir/shared/config.xml")

假設你在 `dev/projects/AcmeHealth` 目錄中處理多專案組建。你在要修正的函式庫的組建中使用上述範例 — 在 `AcmeHealth/subprojects/AcmePatientRecordLib/build.gradle`。檔案路徑將解析為 `dev/projects/AcmeHealth/shared/config.xml` 的絕對版本。

`file()` 方法可被用來設定任何具有 `File` 類型屬性的任務。不過,許多任務會處理多個檔案,因此我們接著來看如何指定檔案組。

檔案集合

檔案集合 僅是一組由 FileCollection 介面表示的檔案路徑。任何 檔案路徑。重要的是,您必須了解檔案路徑不必以任何方式相關,因此它們不必在同一個目錄中,甚至不必有共用父目錄。您還會發現,Gradle API 的許多部分都使用 FileCollection,例如本章稍後討論的複製 API 和 相依性設定

建議使用 ProjectLayout.files(java.lang.Object...) 方法來指定檔案集合,它會傳回 FileCollection 執行個體。此方法非常靈活,讓您可以傳遞多個字串、File 執行個體、字串集合、File 集合等等。您甚至可以傳入任務作為引數,只要它們有 已定義的輸出 即可。在參考指南中了解所有支援的引數類型。

files() 會妥善處理相對路徑和 File(relative path) 執行個體,並根據專案目錄解析它們。

前一節 中介紹的 Project.file(java.lang.Object) 方法一樣,所有相對路徑都是根據目前的專案目錄評估。下列範例示範您可以使用的各種引數類型,例如字串、File 執行個體、清單和 Path

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 的輸出
> 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` 之後使用 `union`,`union` 也會包含那些額外檔案。`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 的輸出
> gradle -q filterTextFiles
src/file1.txt
src/file2.txt
src/file5.txt

如果 `collection` 隨時變更,不論是新增或移除檔案,`textFiles` 都會立即反映變更,因為它也是一個即時集合。請注意,您傳遞給 `filter()` 的閉包會將 `File` 視為引數,並應傳回布林值。

檔案樹狀結構

檔案樹狀結構 是保留其所包含檔案目錄結構的檔案集合,類型為 FileTree。這表示檔案樹狀結構中的所有路徑都必須有共用父目錄。下圖重點說明了在複製檔案的常見情況下,檔案樹狀結構和檔案集合之間的差異

file collection vs file tree
圖 3. 複製檔案時,檔案樹狀結構和檔案集合行為的差異
儘管 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() 的 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 的預設排除是透過 Ant 的 DirectoryScanner 類別設定。
Gradle 不支援在執行階段變更預設排除。

您可以對檔案樹執行許多與檔案集合相同的事情

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

範例 28. 使用檔案樹
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 外掛程式提供它們自己的檔案樹實例,例如 Java 的來源組。這些檔案樹可以用與您自己建立的檔案樹完全相同的方式使用和操作。

使用者通常需要的另一種特定檔案樹類型是檔案,例如 ZIP 檔案、TAR 檔案等。我們接下來會探討這些檔案。

將檔案作為檔案樹使用

檔案是一個目錄和檔案階層,打包成一個單一檔案。換句話說,它是檔案樹的一個特例,而這正是 Gradle 處理檔案的方式。您使用 Project.zipTree(java.lang.Object)Project.tarTree(java.lang.Object) 方法來包裝對應類型的檔案檔案(請注意 JAR、WAR 和 EAR 檔案是 ZIP),而不是使用僅適用於一般檔案系統的 fileTree() 方法。這兩個方法都會傳回 FileTree 實例,然後您可以用與一般檔案樹相同的方式使用這些實例。例如,您可以透過將檔案內容複製到檔案系統上的某個目錄,來提取檔案的部份或全部檔案。或者,您可以將一個檔案合併到另一個檔案中。

以下是一些建立基於檔案的檔案樹的簡單範例

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

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

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

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

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

您可以在我們涵蓋的 常見情境 中,看到提取檔案檔案的實際範例。

了解隱式轉換為檔案集合

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

深入探討檔案複製

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

  • 定義 Copy 類型的任務

  • 指定要複製哪些檔案(和目錄)

  • 指定已複製檔案的目的地

但是,這種表面的簡單性隱藏了一個豐富的 API,它允許精細地控制複製哪些檔案、它們的去向,以及在複製過程中對它們發生的事情,例如檔案重新命名和檔案內容的代碼替換。

讓我們從清單上的最後兩項開始,它們形成所謂的複製規範。這在形式上基於CopySpec介面,Copy任務實作該介面,並提供

CopySpec有幾個額外的允許您控制複製程序的方法,但只有這兩個是必需的。into()很直接,需要一個目錄路徑作為其引數,其形式為Project.file(java.lang.Object)方法所支援的任何形式。from()組態彈性許多。

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

  • 一個String — 視為檔案路徑,或者如果以「file://」開頭,則視為檔案 URI

  • 一個File — 用作檔案路徑

  • 一個FileCollectionFileTree — 複製中包含集合中的所有檔案

  • 一個任務 — 包含任務的已定義輸出的檔案或目錄會包含在內

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

另一個要考慮的是檔案路徑所指的事物的類型

  • 一個檔案 — 檔案會原樣複製

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

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

以下是一個範例,它使用多個from()規範,每個規範具有不同的引數類型。您可能還會注意到,into()是使用閉包(在 Groovy 中)或提供者(在 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()的延遲組態不同於子規範,即使語法相似。注意引數數量以區分它們。

過濾檔案

您已經看到您可以在 Copy 任務中直接篩選檔案集合和檔案樹,但您也可以透過 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 支援正規表示法群組,讓您可以移除和取代原始檔名的一部分。以下範例顯示了如何使用一個簡單的正規表示法,從任何包含「-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. 如果您對第一個引數使用斜線字串(以「/」分隔),則必須包含 rename() 的括號,如上例所示。

  2. 最安全的方法是對第二個引數使用單引號,否則您需要在群組替換中跳脫「$」,即 "\$1\$2"

第一個問題只是一個小不便,但斜線字串的優點是您不必在正規表示式中跳脫反斜線(「\」)字元。第二個問題源自 Groovy 在雙引號和斜線字串中使用 ${ } 語法支援嵌入式表達式。

rename() 的閉包語法很簡單,可用於處理任何單純正規表示式無法處理的要求。您會收到一個檔案名稱,然後傳回該檔案的新名稱,或者如果您不想變更名稱,則傳回 null。請注意,閉包會對每個已複製的檔案執行,因此請盡可能避免昂貴的運算。

過濾檔案內容(代碼替換、範本處理等)

檔案內容過濾與過濾要複製哪些檔案不同,它允許您在複製檔案時轉換檔案內容。這可能涉及使用代碼替換的基本範本處理、移除文字列或使用完整的範本引擎進行更複雜的過濾。

下列範例示範了多種過濾形式,包括使用 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 式樣記號)形式的記號替換為您定義的值。

expand() 方法將原始檔案視為 Groovy 範本,它會評估並擴充 ${expression} 形式的表達式。您可以傳入屬性名稱和值,然後在原始檔案中進行擴充。expand() 允許進行比基本記號替換更多的操作,因為嵌入式表達式是完整的 Groovy 表達式。

在讀寫檔案時,指定字元集是一個良好的習慣,否則轉換無法正常處理非 ASCII 文字。您可以使用 CopySpec.setFilteringCharset(String) 屬性來設定字元集。如果未指定,則會使用 JVM 預設字元集,這可能與您想要的字元集不同。

設定檔案權限

對於任何參與複製檔案的 CopySpec,無論是 Copy 任務本身或任何子規格,您都可以透過 CopySpec.filePermissions {} 組態區塊明確設定目標檔案的權限。您也可以透過 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)

對檔案或目錄權限使用空的組態區塊仍會明確設定權限,只是設定為固定的預設值。事實上,這些組態區塊中的所有內容都與預設值相關。檔案和目錄的預設權限不同

  • 檔案擁有者可讀寫,群組可讀,其他可讀(0644rw-r—​r--

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

使用 CopySpec 類別

複製規格(或簡稱複製規範)決定要複製什麼到哪裡,以及在複製過程中檔案會發生什麼事。您已經在 Copy 和封存任務的設定中看過許多範例。但複製規範有兩個值得更詳細說明的屬性

  1. 它們可以獨立於任務

  2. 它們是階層式的

這些屬性的第一個讓您可以在建置中分享複製規範。第二個則在整體複製規範中提供細緻的控制。

分享複製規範

考慮一個建置,其中有幾個任務會複製專案的靜態網站資源或將它們新增到封存中。一個任務可能會將資源複製到一個資料夾,供本機 HTTP 伺服器使用,而另一個任務可能會將它們打包成發行版。您可以在每次需要時手動指定檔案位置和適當的包含項目,但這樣比較容易發生人為錯誤,導致任務之間不一致。

Gradle 提供的其中一個解決方案是 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() 規格。這不只適用於包含項目,也適用於排除項目、檔案重新命名和檔案內容過濾。

使用子規格

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

exploded war child copy spec example
圖 4. 為 Servlet 容器建立 exploded WAR

這並非直接複製,因為專案中不存在 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()

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

在您自己的工作中複製檔案

在執行時間使用 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() 方法的自訂任務應宣告與複製動作相關的必要輸入和輸出。

使用 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` 任務來部署應用程式封存檔(例如 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")
}

安裝可執行檔

當您建立獨立的可執行檔時,您可能想要將此檔案安裝到您的系統中,以便它出現在您的路徑中。您可以使用 `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")
}

深入探討封存檔建立

封存檔基本上是自給自足的檔案系統,而 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 外掛程式提供的,它預設在 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 的輸出
> gradle -q myZip
archive-naming-1.0.zip
build/distributions
build/distributions/archive-naming-1.0.zip

請注意,封存檔的名稱不會衍生自建立封存檔的工作名稱。

如果您要變更已產生封存檔檔案的名稱和位置,您可以提供對應工作的 archiveFileNamedestinationDirectory 屬性的值。這些值會覆寫原本會套用的任何慣例。

或者,您可以使用 AbstractArchiveTask.getArchiveFileName() 提供的預設封存檔名稱模式:[封存檔基本名稱]-[封存檔附錄]-[封存檔版本]-[封存檔分類器].[封存檔副檔名]。如果您願意,可以在工作中個別設定這些屬性。請注意,Base 外掛程式使用專案名稱的慣例作為 封存檔基本名稱,專案版本作為 封存檔版本,封存檔類型作為 封存檔副檔名。它不會提供其他屬性的值。

這個範例(來自與上述範例相同的專案)只設定 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 的輸出
> 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 的輸出
> 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。當然,如果你希望的話,可以將它設定為自訂的副檔名。

在多個檔案之間分享內容

如前所述,你可以使用 Project.copySpec(org.gradle.api.Action) 方法在檔案之間分享內容。

可重製的建置

有時,需要在不同的機器上以完全相同的位元組對位元組方式重新建立檔案。你想要確保從原始碼建置人工製品會產生相同的結果,無論在何時何地建置。這對於像 reproducible-builds.org 這樣的專案來說是必要的。

重新建立相同的位元組對位元組檔案會帶來一些挑戰,因為檔案在檔案中的順序會受到底層檔案系統的影響。每次從原始碼建置 ZIP、TAR、JAR、WAR 或 EAR 時,檔案在檔案中的順序都可能改變。只有時間戳不同的檔案也會導致建置與建置之間的檔案有所不同。所有與 Gradle 一起提供的 AbstractArchiveTask(例如 Jar、Zip)任務都包含支援產生可重製檔案的功能。

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

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

您通常會想要發布封存,以便從其他專案使用。此程序說明於 跨專案發布 中。