Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SPM Build tools を用いて SwiftGen を導入する上での罠とTips / SPM-SwiftGen-Plugin

Go Takagi
April 25, 2022

SPM Build tools を用いて SwiftGen を導入する上での罠とTips / SPM-SwiftGen-Plugin

集まれSwift好き!Swift愛好会 vol.67 @ オンライン
https://love-swift.connpass.com/event/244413/ の発表資料です

Go Takagi

April 25, 2022
Tweet

More Decks by Go Takagi

Other Decks in Technology

Transcript

  1. SPM Build tools を⽤いて SwiftGen を導⼊する上での罠とTips Go Takagi 20 22

    / 04 / 25 集まれ Swift 好き!Swift 愛好会 vol. 6 7
  2. Me ( Go Takagi ) ‣ ID • shimastripe /

    shimastriper あたり ‣ 仕事 • 新聞社で iOS アプリ開発してます ‣ 好きなエンジニアリング • Infrastructure as Code‧⾃動化とかしてラクしたい ‣ project.xcodeproj を XcodeGen にしたり ‣ Bitrise や GitHub Actions で諸々⾃動化したり 2 前に喋ってから 1年半も経ってしまいました...
  3. Package Manager Extensible Build Tools ‣ Xcode 1 3 .

    3 ( Swift 5 . 6 ) • https://developer.apple.com/documentation/Xcode-Release-Notes/xcode- 1 3 _ 3 - release-notes#Swift-Package-Manager ‣ Proposal • SE- 0 3 0 3 Package Manager Extensible Build Tools • SE- 0 3 0 5 Package Manager Binary Target Improvements • SE- 0 3 3 2 Package Manager Command Plugins • SE- 0 3 2 5 Additional Package Plugin APIs 4
  4. SwiftPM Package Plugins ‣ Swift ( SwiftPM ) 5 .

    6 から登場 • Build tool plugin • ビルド中に利⽤するためのコード⽣成などの処理を実⾏する • UseCase 1 : Protocol Bu ff ers (.proto) から Swift コード⽣成 • UseCase 2 : SwiftGen のような Resource ファイルから Swift コードを⾃動⽣成 • Custom command plugin • 上を拡張して、SwiftPM CLI などからCommand として実⾏できるようにする • ドキュメント⽣成‧コードフォーマット などのコマンドを使えるように 5
  5. UseCase: SwiftGen ‣ Resource ファイルをタイプセーフに扱えるライブラリ • Assets Catalogs / Color

    / Fonts / Localizable.strings 
 
 を安全にアクセスするための Swift ファイルを⽣成してくれる 8 github から引⽤ かわいい public enum Asset { public static let customTextColor = ColorAsset(name: "CustomTextColor") } 毎ビルドいい感じに⽣成 (更新) しておいてほしい
  6. SPM で Resource を扱いたいケース ‣ UI ライブラリでResource を扱う ‣ App

    / AppExtension ( Widgets ) で Resource を共有する • Dynamic Framework にして Resource の重複埋め込みを回避する 9
  7. SwiftGen は SPM でも使える仕組みになっている!! ‣ 通常 SPM の Bundle はアクセスに⼯夫が必要

    → 対応されてる 👍 1 0 public extension ColorAsset.Color { @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) convenience init?(asset: ColorAsset) { let bundle = BundleToken.bundle #if os(iOS) || os(tvOS) self.init(named: asset.name, in: bundle, compatibleWith: nil) #elseif os(macOS) self.init(named: NSColor.Name(asset.name), bundle: bundle) #elseif os(watchOS) self.init(named: asset.name) #endif } } private final class BundleToken { static let bundle: Bundle = { #if SWIFT_PACKAGE return Bundle.module #else return Bundle(for: BundleToken.self) #endif }() }
  8. SPM には Run Script の機構が無い (かった) ‣ ⼯夫が必要 (だった) •

    事前に⽣成したファイルをCommit しておく • Make などセットアップスクリプトで実⾏ • Build 毎に "いい感じに" 再⽣成する快適さが失われる ‣ 別⼿段: Embedded Framework (Cocoa Touch Framework) • こちらは Run Script が実⾏可能 • SDK 依存のツール ( Swift ではない ) 1 1 → SPM Build Tools でようやく快適に動かせる!!
  9. SPM Build Tools を実際に使ってみる ‣ Example を作りました • github.com/shimastripe/SwiftGenPluginExample •

    github.com/shimastripe/SwiftPM-Artifact-Bundle ‣ Hello, world! • ColorKit の Color.xcassets を SwiftGen 経由でアクセス! • 1 2
  10. Plugin.swift を作ってコマンドを実装する 1 5 @main struct SwiftGenPlugin: BuildToolPlugin { func

    createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { let outputFilesDirectory = context.pluginWorkDirectory let targetAssets = target.directory.appending("Resources/Color.xcassets") let outputFile = outputFilesDirectory.appending("Color.generated.swift") return [ .prebuildCommand( displayName: "SwiftGen", executable: try context.tool(named: "swiftgen").path, arguments: [ "run", "xcassets", targetAssets.string, "--param", "publicAccess", "--templateName", "swift5", "--output", outputFile.string, ], environment: [:], outputFilesDirectory: outputFilesDirectory) ] } } swiftgen コマンドの実⾏
  11. Resolve Packages 時に実⾏される ‣ Plugin の createBuildCommand() が呼ばれる •             

    が Fail するとこの段階で失敗する • return [ .preBuildCommand() ] が実⾏されるわけではなく、登録されます 1 6 try context.tool(named: "swiftgen")
  12. context.tool → SwiftGen binaryTarget を設定 ‣ Package.swift でバイナリコマンドを設定する • ArtifactBundle

    という manifest を⽤意する必要がある 1 7 let package = Package( ... targets: [ .binaryTarget( name: "swift-cli-tools", url: "https://github.com/shimastripe/SwiftPM-Artifact-Bundle/releases/download/0.2.1/swift-cli-tools.artifactbundle.zip", checksum: "f8a9d286b891ba8981ddd9cb1a7ceaa45e9385976b310d49ef62bdc05a704e0c"), .plugin( name: "SwiftGenPlugin", capability: .buildTool(), dependencies: ["swift-cli-tools"]), .target( name: "ColorKit", dependencies: [], resources: [ .process("Resources"), ], plugins: [.plugin(name: "SwiftGenPlugin")]), ]) Package.swift
  13. BinaryTarget は Artifact Bundle の⽤意が必要 ‣ Example • github.com/shimastripe/SwiftPM-Artifact-Bundle ‣

    Artifact Bundle • JSON に バイナリとサポートOSの対応を記載したmanifestを⽤意 ‣ checksum を計算して、Package.swift で指定 • Artifact Bundle を含んだ zip を⽤意 • $ swift package compute-checksum ./Artifact.zip • Package.swift がある位置で実⾏しないといけないことに注意 1 8 { "schemaVersion": "1.0", "artifacts": { "swiftgen": { "type": "executable" , "version": "6.5.1" , "variants": [ { "path": "swiftgen-6.5.1-macos/bin/swiftgen" , "supportedTriples": [ "x86_64-apple-macosx" , "arm64-apple-macosx" ] } ] } } } <name>.artifactbundle ├ info.json 各CLIツールでこれをやらないといけないの...?
  14. (FYI) 各種 CLI ツールも対応 (準備) 進んでます ‣ SwiftLint • #

    3 84 0 [Suggestion] - Swift Package Manager build tool support ‣ SwiftGen • # 9 26 Support Swift Package Plugins: Adds a task to include a .artifactbundle with the release assets ‣ R.swift • # 7 49 Generate resources of a Swift Package (without the need of Xcodeproj) 1 9
  15. これで動かせる!! ‣ 利⽤できる Command は PreBuild / Build の 2種類

    • PreBuild : Build の最初に毎回実⾏される • Build: 指定している InputFiles が Changed / OutputFiles が Missing 2 0 @main struct SwiftGenPlugin: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { let outputFilesDirectory = context.pluginWorkDirectory let targetAssets = target.directory.appending("Resources/Color.xcassets") let outputFile = outputFilesDirectory.appending("Color.generated.swift") return [ .prebuildCommand( displayName: "SwiftGen", executable: try context.tool(named: "swiftgen").path, arguments: [ "run", "xcassets", targetAssets.string, "--param", "publicAccess", "--templateName", "swift5", "--output", outputFile.string, ], environment: [:], outputFilesDirectory: outputFilesDirectory) ] } } PreBuild の⽅が パフォーマンスに影響与えやすい ↓ 出⼒ファイル名がわからないときのみが 良いとコメントされない パラメータもDirectory 単位
  16. Build tool ⽣成ファイルの制約 ‣ context.pluginWorkDirectory 以下にのみ⽣成できる • Xcode : ~/Library/Developer/Xcode/DerivedData/...

    • CLI : ./.build/... ‣ (Command Plugin) Code Formatter のような場合 • Permission をつけて Package のコードも書き換えられる • $ swift package plugin <command> 
 --allow-writing-to-package-directory --allow-writing-to-directory <allow-writing-to-directory> 2 5 .plugin( name: "Formatter", capability: .command(intent: .sourceCodeFormatting(), permissions: [.writeToPackageDirectory(reason: "")]), dependencies: ["swift-cli-tools"]), Package.swift
  17. (Project が複雑だと? ) Xcode上で Environment が渡せない ‣ BuildCommand は環境変数を渡せる 2

    6 @main struct SwiftGenPlugin: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { let outputFilesDirectory = context.pluginWorkDirectory let targetAssets = target.directory.appending("Resources/Color.xcassets") let outputFile = outputFilesDirectory.appending("Color.generated.swift") return [ .prebuildCommand( displayName: "SwiftGen", executable: try context.tool(named: "swiftgen").path, arguments: [ "run", "xcassets", targetAssets.string, "--param", "publicAccess", "--templateName", "swift5", "--output", outputFile.string, ], environment: [:], outputFilesDirectory: outputFilesDirectory) ] } } ⼤きめのプロジェクトで環境変数が認識されないケースがあった
  18. (Project が複雑だと? ) PreBuild が⾛らない 😇 ‣ PreBuild のステップそのものが発⽣しなくなる •

    2 Project で発⽣、ColorKit を引っ張っても動かない • OSSで再現しようとしたんですが再現できず... • xcodeproj ⾃体は XcodeGen などで作り直しても⾛らない ‣ 回避するには...... • 事前にビルド • xcodebuild コマンドを中で呼ぶとか..... → これはできない • 2 7 これ頑張ってたらRunScriptの 意味がないじゃん 😇 ステップそのものが呼ばれなくなる
  19. 四苦⼋苦して Workaround を⾒つける ‣ Build は動く事が判明 • SwiftGenの場合 問題なし 2

    8 return [ .prebuildCommand( displayName: "SwiftGen", executable: try context.tool(named: "swiftgen").path, arguments: [ "run", "xcssets", targetAssets.string, "--param", "publicAccess", "--templateName", "swift5", "--output", outputFile.string, ], environment: [:], outputFilesDirectory: outputFilesDirectory), return [ .buildCommand( displayName: "SwiftGen", executable: try context.tool(named: "swiftgen").path, arguments: [ "run", "xcassets", targetAssets.string, "--param", "publicAccess", "--templateName", "swift5", "--output", outputFile.string, ], environment: [:], outputFiles: [outputFile] ),
  20. (FYI) 早速 Apple の OSS でも使われている! ‣ apple/swift-sample-distributed-actors-transport • takes

    care of source generating the necessary "glue" between distributed functions and the transport runtime. 2 9 https://github.com/apple/swift-sample-distributed-actors-transport#swiftpm-plugin let command = Command.buildCommand( displayName: "Distributed Actors: Generating FISHY actors for \(context.targetName)", executable: generatorPath, arguments: [ "--verbose", "--source-directory", context.targetDirectory.string, "--target-directory", context.pluginWorkDirectory.string, "--buckets", "\(buckets)", ], inputFiles: inputFiles, outputFiles: outputFiles )
  21. 今の所 ちょっとデバッグが⾟い ‣ 挙動の理解が難しい • Report Navigator タブを開いて探さないといけない • コマンドの書き⽅がおかしくても、どの段階で⽌まるかエラーも明瞭ではない

    ‣ Example 活⽤してください • ⼩さく初めて動作確認しつつ進めたほうが良い • (説明した通り) 導⼊するProject 側の影響で動かないとかあったので........ 3 0
  22. まとめ ‣ SPM Build Tool Plugins • SwiftGen のコード⽣成を実際に動かしてみました •

    いくつか問題や回避⽅法を紹介 • Example Project も⽤意したのでぜひ触ってみてください ‣ デバッグ⾟いけど楽しいのでやってみましょう • 完成してシュッと動くと気持ちいいです 😂 3 1