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

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

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Go Takagi Go Takagi
April 25, 2022

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

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

Avatar for Go Takagi

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