您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何使用Swift Package插件生成代碼”,在日常操作中,相信很多人在如何使用Swift Package插件生成代碼問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何使用Swift Package插件生成代碼”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
不久前,我正在工作中開發一項新服務,該服務由 Swift Package 組成,該 Package 公開了一個類似于Decodable協議,供我們應用程序的其余部分使用。事實上,該協議是從Decodable本身繼承下來的,看起來像這樣:
Fetchable.swit
protocol Fetchable: Decodable, Equatable {}
新的 package 將采用符合Fetchable的類型來嘗試從遠程或緩存的JSON數據塊中解碼它們。
由于這項服務對應用程序的正確運行至關重要,作為這項工作的一部分,我們希望確保始終存在故障安全( fail-safe)。因此,我們讓該應用程序附帶了一個備用的JSON文件,如果遠程和緩存的數據解碼失敗,將使用該文件,來保證程序的正常運行。
無論如何,我們需要符合Fetchable的新類型從備用數據中正確解碼。然而,有一個問題,有時很難發現備用JSON文件或模型本身是否有任何錯誤,因為解碼錯誤會在運行時發生,并且只有在訪問某些屏幕/功能時才會發生。
為了讓我們對我們要發送的代碼更有信心,我們添加了一些單元測試,試圖根據我們附帶的備用JSON解碼符合Fetchable協議的每個模型。這些將使我們在CI上有一個早期指示,表明備用數據或模型中存在錯誤,如果所有測試都通過,我們將確定,一旦我們發布新服務,它始終具有故障安全功能。
我們手動編寫了這些測試,但我們很快就意識到這個解決方案是不可擴展的,因為隨著越來越多的符合Fetchable協議的類型被添加,我們引入了大量的代碼復制,并可能有人最終忘記為特定功能編寫這些測試。
我們考慮過自動化該過程,但由于我們的代碼庫的性質,我們遇到了一些問題,代碼庫高度模塊化,混合了Xcode項目和Swift Package。一些架構決策還意味著我們必須收集大量符號信息,才能獲得生成測試的正確類型。
在我忘記了這件事一段時間后,Xcode 14的公告允許在Xcode項目中使用 Swift Package 插件,以及一些架構更改使提取類型信息變得容易得多,這讓我有動力再次開始研究這個問題。
請注意,Xcode項目的構建工具插件尚未按照發布說明在Xcode 14 Beta 2中提供,但將在Xcode 14的未來版本中提供。
圖片取自 Xcode Beta 2 版的發布說明
在過去的幾周里,我一直在研究如何使用軟件包插件生成單元測試,在這篇文章中,我將解釋我在向哪個方向嘗試以及它涉及了什么。
我開始了一項任務,即創建一個構建工具插件,與 Xcode 14 引入的命令插件不同,該插件可以任意運行并依賴用戶輸入,作為Swift軟件包構建過程的一部分運行。
我知道我需要創建一個可執行文件,因為 Build Tool 插件依賴這些來執行操作。這個腳本將完全用 Swift 編寫,因為這是我最熟悉的語言,并承擔以下職責:
掃描目標目錄并提取所有.swift文件。目標將被遞歸掃描,以確保不會錯過子目錄。
使用sourcekit,或者更具體地說,SourceKitten,掃描這些.swift文件并收集類型信息。這將允許提取符合Fetchable協議的所有類型,以便可以針對它們編寫測試。
獲得這些類型后,生成一個帶有XCTestCase的.swift文件,其中包含每種類型的單元測試。
與所有 Swift Package 一樣,最簡單的入門方法是在命令行上運行swift package init。
這創建了兩個目標,一個是包含Fetchable協議定義和符合該定義的類型的實現代碼,另一個是應用插件為此類類型生成單元測試的測試目標。
Package.swit
// swift-tools-version: 5.6 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "CodeGenSample", platforms: [.macOS(.v10_11)], products: [ .library( name: "CodeGenSample", targets: ["CodeGenSample"]), ], dependencies: [ ], targets: [ .target( name: "CodeGenSample", dependencies: [] ), .testTarget( name: "CodeGenSampleTests", dependencies: ["CodeGenSample"] ) ] )
如前所述,所有構建工具插件都需要可執行文件來執行所有必要的操作。
為了幫助開發此命令行,將使用幾個依賴項。第一個是SourceKitten——特別是其SourceKitten框架庫,這是一個Swift包裝器,用于幫助使用Swift代碼編寫sourcekit請求,第二個是快速參數解析器,這是蘋果提供的軟件包,可以輕松創建命令行工具,并以更快、更安全的方式解析在執行過程中傳遞的命令行參數。
在創建executableTarget并賦予它兩個依賴項后,Package.swift就是這個樣子:
Package.swift
// swift-tools-version: 5.6 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "CodeGenSample", platforms: [.macOS(.v10_11)], products: [ .library( name: "CodeGenSample", targets: ["CodeGenSample"]), ], dependencies: [ .package(url: "https://github.com/jpsim/SourceKitten.git", exact: "0.32.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0") ], targets: [ .target( name: "CodeGenSample", dependencies: [] ), .testTarget( name: "CodeGenSampleTests", dependencies: ["CodeGenSample"] ), .executableTarget( name: "PluginExecutable", dependencies: [ .product(name: "SourceKittenFramework", package: "SourceKitten"), .product(name: "ArgumentParser", package: "swift-argument-parser") ] ) ] )
可執行目標需要一個入口點,因此,在PluginExecutable目標的源目錄下,必須創建一個名為PluginExecutable.swift的文件,其中所有可執行邏輯都需要創建。
請注意,這個文件可以隨心所欲地命名,我傾向于以與我在Package.swift中創建的目標相同的方式命名它。
如下所示的腳本導入必要的依賴項,并創建可執行文件的入口點(必須用@main裝飾),并聲明在執行時傳遞的4個輸入。
所有邏輯和方法調用都存在于run函數中,該函數是調用可執行文件時運行的方法。這是ArgumentParser語法的一部分,如果您想了解更多信息,Andy Ibañez有一篇關于該主題的精彩文章,可能非常有幫助。
PluginExecutable.swift
import SourceKittenFramework import ArgumentParser import Foundation @main struct PluginExecutable: ParsableCommand { @Argument(help: "The protocol name to match") var protocolName: String @Argument(help: "The module's name") var moduleName: String @Option(help: "Directory containing the swift files") var input: String @Option(help: "The path where the generated files will be created") var output: String func run() throws { // 1 let files = try deepSearch(URL(fileURLWithPath: input, isDirectory: true)) // 2 setenv("IN_PROCESS_SOURCEKIT", "YES", 1) let structures = try files.map { try Structure(file: File(path: $0.path)!) } // 3 var matchedTypes = [String]() structures.forEach { walkTree(dictionary: $0.dictionary, acc: &matchedTypes) } // 4 try createOutputFile(withContent: matchedTypes) } // ... }
現在讓我們專注于上面的run方法,以了解當插件運行可執行文件時會發生什么:
首先,掃描目標目錄以找到其中的所有.swift文件。這是遞歸完成的,這樣子目錄就不會錯過。此目錄的路徑作為參數傳遞給可執行文件。
對于上次調用中找到的每個文件,通過SourceKitten發出Structure請求,以查找文件中Swift代碼的類型信息。請注意,環境變量(IN_PROCESS_SOURCEKIT)也被設置為true。這需要確保選擇源套件的進程中版本,以便它能夠遵守插件的沙盒規則。
Xcode附帶兩個版本的sourcekit可執行文件,一個版本解析進程中的文件,另一個使用XPC向解析進程外文件的守護進程發送請求。后者是mac上的默認版本,為了能夠將sourcekit用作插件進程的一部分,必須選擇進程中版本。這最近在SourceKitten上作為環境變量實現,是運行引擎蓋下使用sourcekit的其他可執行文件的關鍵,例如SwiftLint。
瀏覽上次調用的所有響應,并掃描類型信息以提取符合Fetchable協議的任何類型。
在傳遞給可執行文件的output參數指定的位置創建一個輸出文件,其中包含每種類型的單元測試。
請注意,上面沒有重點介紹每個調用的具體細節,但如果你對實現感興趣,包含所有代碼的repo現在已經在Github上公開了!
與可執行文件一樣,必須向Package.swift添加.plugin目標,并且必須創建包含插件實現的.swift文件(Plugins/SourceKitPlugin/SourceKitPlugin.swift)。
Package.swift
// swift-tools-version: 5.6 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "CodeGenSample", platforms: [.macOS(.v10_11)], products: [ .library( name: "CodeGenSample", targets: ["CodeGenSample"]), ], dependencies: [ .package(url: "https://github.com/jpsim/SourceKitten.git", exact: "0.32.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0") ], targets: [ .target( name: "CodeGenSample", dependencies: [] ), .testTarget( name: "CodeGenSampleTests", dependencies: [“CodeGenSample"], plugins: [“SourceKitPlugin”], ), .executableTarget( name: "PluginExecutable", dependencies: [ .product(name: "SourceKittenFramework", package: "SourceKitten"), .product(name: "ArgumentParser", package: "swift-argument-parser") ] ), .plugin( name: "SourceKitPlugin", capability: .buildTool(), dependencies: [.target(name: "PluginExecutable")] ) ] )
以下代碼顯示了插件的初始實現,其struct符合BuildToolPlugin的協議。這需要實現一個返回具有單個構建命令的數組的createBuildCommands方法。
此插件使用buildCommand而不是preBuildCommand,因為它需要作為構建過程的一部分運行,而不是在它之前運行,因此它有機會構建和使用它所依賴的可執行文件。在這種情況下,支持使用buildCommand的另一點是,它只會在輸入文件更改時運行,而不是每次構建目標時運行。
此命令必須為要運行的可執行文件提供名稱和路徑,這可以在插件的上下文中找到:
SourceKitPlugin.swift
import PackagePlugin @main struct SourceKitPlugin: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { return [ .buildCommand( displayName: "Protocol Extraction!", executable: try context.tool(named: "PluginExecutable").path, arguments: [ "FindThis", , "--input", , "--output", ], environment: ["IN_PROCESS_SOURCEKIT": "YES"], outputFiles: [ ] ) ] } }
如上面的代碼所示,還有一些空白需要填充( ):
提供outputPath,用于生成單元測試文件。此文件可以在pluginWorkDirectory中生成,也可以在插件的上下文中找到。該目錄提供讀寫權限且其中創建的任何文件都將是軟件包構建過程的一部分。
提供輸入路徑和模塊名稱。這是最棘手的部分,這些需要指向正在測試的目標的來源,而不是插件正在應用于的目標——單元測試。謝天謝地,插件的目標依賴項是可訪問的,我們可以從該數組中獲取我們感興趣的依賴項。此依賴項將是內部的(target而不是product),它將為可執行文件提供其名稱和目錄。
SourceKitPlugin.swift
import PackagePlugin @main struct SourceKitPlugin: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { let outputPath = context.pluginWorkDirectory.appending(“GeneratedTests.swift”) guard let dependencyTarget = target .dependencies .compactMap { dependency -> Target? in switch dependency { case .target(let target): return target default: return nil } } .filter { "\($0.name)Tests" == target.name } .first else { Diagnostics.error("Could not get a dependency to scan!”) return [] } return [ .buildCommand( displayName: "Protocol Extraction!", executable: try context.tool(named: "PluginExecutable").path, arguments: [ "Fetchable", dependencyTarget.name, "--input", dependencyTarget.directory, "--output", outputPath ], environment: ["IN_PROCESS_SOURCEKIT": "YES"], outputFiles: [outputPath] ) ] } }
注意上述可選性處理方式。如果在測試目標的依賴項中找不到 合適的 目標,則使用Diagnostics API將錯誤轉發回Xcode,并告訴它完成構建過程。
插件這就完成了!現在讓我們在 Xcode 中運行它!為了測試這種方法,將包含以下內容的文件添加到CodeGenSample目標中:
CodeGenSample.swift
import Foundation protocol Fetchable: Decodable, Equatable {} struct FeatureABlock: Fetchable { let featureA: FeatureA struct FeatureA: Fetchable { let url: URL } } enum Root { struct RootBlock: Fetchable { let url: URL let areAllFeaturesEnabled: Bool } }
請注意,腳本將在結構中首次出現Fetchable協議時停止。這意味著任何嵌套的符合Fetchable協議的類型都將被測試,只是外部模型。
給定此輸入并在主目標上運行測試,生成并運行XCTestCase,其中包含符合Fetchable協議的兩種類型的測試。
GeneratedTests.swift
import XCTest @testable import CodeGenSample class GeneratedTests: XCTestCase { func testFeatureABlock() { assertCanParseFromDefaults(FeatureABlock.self) } func testRoot_RootBlock() { assertCanParseFromDefaults(Root.RootBlock.self) } private func assertCanParseFromDefaults<T: Fetchable>(_ type: T.Type) { // Logic goes here... } }
所有測試都通過了:sweat_smile::white_check_mark:而且,盡管他們目前沒有做很多事情,但可以擴展實現,以提供一些示例數據和一個JSONDecoder實例來對每個單元測試進行解析。
到此,關于“如何使用Swift Package插件生成代碼”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。