Slide 1

Slide 1 text

SPM Build tools を⽤いて SwiftGen を導⼊する上での罠とTips Go Takagi 20 22 / 04 / 25 集まれ Swift 好き!Swift 愛好会 vol. 6 7

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Agenda ‣ SPM Build Tool の話がしたい! • ファイル⽣成編 • デバッグが少し難しいから簡単に実⾏ (実装) の流れを解説します • 踏み抜いた罠‧Tips 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

そう前回のSwift愛好会でも usami さんが ‣ Swift-DocC の command plugin でドキュメント⽣成 • https://speakerdeck.com/usamik 2 6 /try-swift-docc-plugin 6

Slide 7

Slide 7 text

アプリの場合、Run Script 7

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

SPM で Resource を扱いたいケース ‣ UI ライブラリでResource を扱う ‣ App / AppExtension ( Widgets ) で Resource を共有する • Dynamic Framework にして Resource の重複埋め込みを回避する 9

Slide 10

Slide 10 text

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 }() }

Slide 11

Slide 11 text

SPM には Run Script の機構が無い (かった) ‣ ⼯夫が必要 (だった) • 事前に⽣成したファイルをCommit しておく • Make などセットアップスクリプトで実⾏ • Build 毎に "いい感じに" 再⽣成する快適さが失われる ‣ 別⼿段: Embedded Framework (Cocoa Touch Framework) • こちらは Run Script が実⾏可能 • SDK 依存のツール ( Swift ではない ) 1 1 → SPM Build Tools でようやく快適に動かせる!!

Slide 12

Slide 12 text

SPM Build Tools を実際に使ってみる ‣ Example を作りました • github.com/shimastripe/SwiftGenPluginExample • github.com/shimastripe/SwiftPM-Artifact-Bundle ‣ Hello, world! • ColorKit の Color.xcassets を SwiftGen 経由でアクセス! • 1 2

Slide 13

Slide 13 text

動作イメージ 1 3

Slide 14

Slide 14 text

実装の流れ

Slide 15

Slide 15 text

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 コマンドの実⾏

Slide 16

Slide 16 text

Resolve Packages 時に実⾏される ‣ Plugin の createBuildCommand() が呼ばれる •              が Fail するとこの段階で失敗する • return [ .preBuildCommand() ] が実⾏されるわけではなく、登録されます 1 6 try context.tool(named: "swiftgen")

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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" ] } ] } } } .artifactbundle ├ info.json 各CLIツールでこれをやらないといけないの...?

Slide 19

Slide 19 text

(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

Slide 20

Slide 20 text

これで動かせる!! ‣ 利⽤できる 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 単位

Slide 21

Slide 21 text

実際にファイルが⽣成された!! ‣ Build の最初で SwiftGen が実⾏されている • DerivedData に Color.generated.swift が⽣成 • .gitignore する必要もない 2 1

Slide 22

Slide 22 text

成功すると、File Jump も効くようになる 2 2

Slide 23

Slide 23 text

これで無事完成ですね!!

Slide 24

Slide 24 text

これで無事完成ですね!! 踏みぬいた罠を紹介します 😇

Slide 25

Slide 25 text

Build tool ⽣成ファイルの制約 ‣ context.pluginWorkDirectory 以下にのみ⽣成できる • Xcode : ~/Library/Developer/Xcode/DerivedData/... • CLI : ./.build/... ‣ (Command Plugin) Code Formatter のような場合 • Permission をつけて Package のコードも書き換えられる • $ swift package plugin 
 --allow-writing-to-package-directory --allow-writing-to-directory 2 5 .plugin( name: "Formatter", capability: .command(intent: .sourceCodeFormatting(), permissions: [.writeToPackageDirectory(reason: "")]), dependencies: ["swift-cli-tools"]), Package.swift

Slide 26

Slide 26 text

(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) ] } } ⼤きめのプロジェクトで環境変数が認識されないケースがあった

Slide 27

Slide 27 text

(Project が複雑だと? ) PreBuild が⾛らない 😇 ‣ PreBuild のステップそのものが発⽣しなくなる • 2 Project で発⽣、ColorKit を引っ張っても動かない • OSSで再現しようとしたんですが再現できず... • xcodeproj ⾃体は XcodeGen などで作り直しても⾛らない ‣ 回避するには...... • 事前にビルド • xcodebuild コマンドを中で呼ぶとか..... → これはできない • 2 7 これ頑張ってたらRunScriptの 意味がないじゃん 😇 ステップそのものが呼ばれなくなる

Slide 28

Slide 28 text

四苦⼋苦して 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] ),

Slide 29

Slide 29 text

(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 )

Slide 30

Slide 30 text

今の所 ちょっとデバッグが⾟い ‣ 挙動の理解が難しい • Report Navigator タブを開いて探さないといけない • コマンドの書き⽅がおかしくても、どの段階で⽌まるかエラーも明瞭ではない ‣ Example 活⽤してください • ⼩さく初めて動作確認しつつ進めたほうが良い • (説明した通り) 導⼊するProject 側の影響で動かないとかあったので........ 3 0

Slide 31

Slide 31 text

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