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

    View full-size slide

  2. Me ( Go Takagi )
    ‣ ID


    • shimastripe / shimastriper あたり


    ‣ 仕事


    • 新聞社で iOS アプリ開発してます


    ‣ 好きなエンジニアリング


    • Infrastructure as Code‧⾃動化とかしてラクしたい


    ‣ project.xcodeproj を XcodeGen にしたり


    ‣ Bitrise や GitHub Actions で諸々⾃動化したり
    2
    前に喋ってから


    1年半も経ってしまいました...

    View full-size slide

  3. Agenda
    ‣ SPM Build Tool の話がしたい!


    • ファイル⽣成編


    • デバッグが少し難しいから簡単に実⾏ (実装) の流れを解説します


    • 踏み抜いた罠‧Tips
    3

    View full-size slide

  4. 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

    View full-size slide

  5. 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

    View full-size slide

  6. そう前回のSwift愛好会でも usami さんが
    ‣ Swift-DocC の command plugin でドキュメント⽣成


    • https://speakerdeck.com/usamik
    2 6
    /try-swift-docc-plugin
    6

    View full-size slide

  7. アプリの場合、Run Script
    7

    View full-size slide

  8. UseCase: SwiftGen
    ‣ Resource ファイルをタイプセーフに扱えるライブラリ


    • Assets Catalogs / Color / Fonts / Localizable.strings


    を安全にアクセスするための Swift ファイルを⽣成してくれる
    8
    github から引⽤


    かわいい
    public enum Asset {


    public static let customTextColor = ColorAsset(name: "CustomTextColor")


    }
    毎ビルドいい感じに⽣成 (更新) しておいてほしい

    View full-size slide

  9. SPM で Resource を扱いたいケース
    ‣ UI ライブラリでResource を扱う


    ‣ App / AppExtension ( Widgets ) で Resource を共有する


    • Dynamic Framework にして Resource の重複埋め込みを回避する
    9

    View full-size slide

  10. 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


    }()


    }

    View full-size slide

  11. SPM には Run Script の機構が無い (かった)
    ‣ ⼯夫が必要 (だった)


    • 事前に⽣成したファイルをCommit しておく


    • Make などセットアップスクリプトで実⾏


    • Build 毎に "いい感じに" 再⽣成する快適さが失われる


    ‣ 別⼿段: Embedded Framework (Cocoa Touch Framework)


    • こちらは Run Script が実⾏可能


    • SDK 依存のツール ( Swift ではない )


    1
    1
    → SPM Build Tools でようやく快適に動かせる!!

    View full-size slide

  12. SPM Build Tools を実際に使ってみる
    ‣ Example を作りました


    • github.com/shimastripe/SwiftGenPluginExample


    • github.com/shimastripe/SwiftPM-Artifact-Bundle


    ‣ Hello, world!


    • ColorKit の Color.xcassets を SwiftGen 経由でアクセス!



    1
    2

    View full-size slide

  13. 動作イメージ
    1
    3

    View full-size slide

  14. 実装の流れ

    View full-size slide

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

    View full-size slide

  16. Resolve Packages 時に実⾏される
    ‣ Plugin の createBuildCommand() が呼ばれる


    •              が Fail するとこの段階で失敗する


    • return [ .preBuildCommand() ] が実⾏されるわけではなく、登録されます
    1
    6
    try context.tool(named: "swiftgen")

    View full-size slide

  17. 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

    View full-size slide

  18. 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ツールでこれをやらないといけないの...?

    View full-size slide

  19. (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

    View full-size slide

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

    View full-size slide

  21. 実際にファイルが⽣成された!!
    ‣ Build の最初で SwiftGen が実⾏されている


    • DerivedData に Color.generated.swift が⽣成


    • .gitignore する必要もない
    2
    1

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  25. 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

    View full-size slide

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


    ]


    }


    }
    ⼤きめのプロジェクトで環境変数が認識されないケースがあった

    View full-size slide

  27. (Project が複雑だと? ) PreBuild が⾛らない 😇
    ‣ PreBuild のステップそのものが発⽣しなくなる



    2
    Project で発⽣、ColorKit を引っ張っても動かない


    • OSSで再現しようとしたんですが再現できず...


    • xcodeproj ⾃体は XcodeGen などで作り直しても⾛らない


    ‣ 回避するには......


    • 事前にビルド


    • xcodebuild コマンドを中で呼ぶとか..... → これはできない



    2
    7
    これ頑張ってたらRunScriptの


    意味がないじゃん 😇
    ステップそのものが呼ばれなくなる

    View full-size slide

  28. 四苦⼋苦して 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]


    ),

    View full-size slide

  29. (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


    )


    View full-size slide

  30. 今の所 ちょっとデバッグが⾟い
    ‣ 挙動の理解が難しい


    • Report Navigator タブを開いて探さないといけない


    • コマンドの書き⽅がおかしくても、どの段階で⽌まるかエラーも明瞭ではない


    ‣ Example 活⽤してください


    • ⼩さく初めて動作確認しつつ進めたほうが良い


    • (説明した通り) 導⼊するProject 側の影響で動かないとかあったので........
    3
    0

    View full-size slide

  31. まとめ
    ‣ SPM Build Tool Plugins


    • SwiftGen のコード⽣成を実際に動かしてみました


    • いくつか問題や回避⽅法を紹介


    • Example Project も⽤意したのでぜひ触ってみてください


    ‣ デバッグ⾟いけど楽しいのでやってみましょう


    • 完成してシュッと動くと気持ちいいです 😂
    3
    1

    View full-size slide