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

Generating Code And Other Mischief With Swift Package Manager Plugins - 360iDev, Denver, CO, August 2022

Generating Code And Other Mischief With Swift Package Manager Plugins - 360iDev, Denver, CO, August 2022

It’s the build tooling step everyone hates: “Now, add a new Run Script Build Phase.” The Swift and Xcode teams have worked to try to make things at least a little bit better for things which need to happen at build time by adding Build Tool Plugins and Command Plugins to Swift Package manager.

In this talk, you’ll get a look at how to set these up to generate code and documentation, plus a look at some other silly things you can make it do.

Sample App: https://github.com/designatednerd/RadioactiveCat
Sample Plugin: https://github.com/designatednerd/DatedImageGenerator

Ellen Shapiro
PRO

August 30, 2022
Tweet

More Decks by Ellen Shapiro

Other Decks in Technology

Transcript

  1. GENERATING CODE AND OTHER MISCHIEF WITH
    SWIFT PACKAGE MANAGER PLUGINS
    360IDEV | DENVER, CO | AUGUST 2022
    ELLEN SHAPIRO | @DESIGNATEDNERD | GUSTO.COM

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. ALCATRAZ

    View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  17. View Slide

  18. !
    XCODE GHOST

    View Slide

  19. !
    XCODE GHOST

    View Slide

  20. !
    STRAWHORSE

    View Slide

  21. View Slide

  22. View Slide

  23. !

    View Slide

  24. View Slide

  25. XCODE SOURCE EXTENSIONS

    View Slide

  26. REQUIRED HOST MAC APP

    View Slide

  27. REQUIRED HOST MAC APP

    !
    Can now be distributed easily on the Mac App store

    View Slide

  28. REQUIRED HOST MAC APP

    !
    Can now be distributed easily on the Mac App store

    "
    Sandboxing restrictions

    View Slide

  29. REQUIRED HOST MAC APP

    !
    Can now be distributed easily on the Mac App store

    "
    Sandboxing restrictions

    #
    Often winds up with an empty app

    View Slide

  30. func perform(with invocation: XCSourceEditorCommandInvocation,
    completionHandler: @escaping (Error?) -> Void) {
    // Actual code of plugin
    }

    View Slide

  31. View Slide

  32. ONLY SINGLE-FILE TEXT BUFFER ACCESS

    View Slide

  33. ONLY SINGLE-FILE TEXT BUFFER ACCESS

    !
    Does allow at least some sorting, linting, formatting

    View Slide

  34. ONLY SINGLE-FILE TEXT BUFFER ACCESS

    !
    Does allow at least some sorting, linting, formatting


    Only ever the current file

    View Slide

  35. ONLY SINGLE-FILE TEXT BUFFER ACCESS

    !
    Does allow at least some sorting, linting, formatting


    Only ever the current file

    #
    No access to the AST

    View Slide

  36. ONLY SINGLE-FILE TEXT BUFFER ACCESS

    !
    Does allow at least some sorting, linting, formatting


    Only ever the current file

    #
    No access to the AST

    $
    No access to file or project structure

    View Slide

  37. ONLY SINGLE-FILE TEXT BUFFER ACCESS

    !
    Does allow at least some sorting, linting, formatting


    Only ever the current file

    #
    No access to the AST

    $
    No access to file or project structure

    $
    No user interface permitted

    View Slide

  38. FURTHER INVOCATION LIMITATIONS

    View Slide

  39. FURTHER INVOCATION LIMITATIONS

    !
    Cannot run in background

    View Slide

  40. FURTHER INVOCATION LIMITATIONS

    !
    Cannot run in background

    "
    Must be actively invoked by the user

    View Slide

  41. FURTHER INVOCATION LIMITATIONS

    !
    Cannot run in background

    "
    Must be actively invoked by the user

    #
    No default key binding

    View Slide

  42. View Slide

  43. View Slide

  44. View Slide

  45. View Slide

  46. View Slide

  47. SWIFT PACKAGE MANAGER
    !
    PLUGINS

    View Slide

  48. View Slide

  49. View Slide

  50. View Slide

  51. View Slide


  52. SWIFT 5.6

    View Slide


  53. SWIFT 5.6
    !
    XCODE 14

    View Slide

  54. 1. COMMAND PLUGINS
    2. BUILD TOOL PLUGINS

    View Slide

  55. COMMAND PLUGINS

    View Slide

  56. CODE FROM "CREATE SWIFT PACKAGE PLUGINS"
    HTTPS://DEVELOPER.APPLE.COM/VIDEOS/PLAY/WWDC2022/110401/

    View Slide

  57. COMMAND PLUGINS
    ▸ Can ask permission to write in the package directory

    View Slide

  58. // Package.swift
    .plugin(
    name: "GenerateContributors",
    capability: .command(
    intent: .custom(verb: "regenerate-contributors-list",
    description: "Generates the CONTRIBUTORS.txt
    file based on Git logs"),
    permissions: [
    .writeToPackageDirectory(reason: "This command write the
    new CONTRIBUTORS.txt to the source root.")
    ]
    )),

    View Slide

  59. // Package.swift
    .plugin(
    name: "GenerateContributors",
    capability: .command(
    intent: .custom(verb: "regenerate-contributors-list",
    description: "Generates the CONTRIBUTORS.txt
    file based on Git logs"),
    permissions: [
    .writeToPackageDirectory(reason: "This command write the
    new CONTRIBUTORS.txt to the source root.")
    ]
    )),

    View Slide

  60. COMMAND PLUGINS
    ▸ Can ask permission to write in the package directory
    ▸ Can be invoked at the command line

    View Slide

  61. // Get all plugins available on a particular package
    swift package plugin --list
    // Run a specific plugin, in this case GenerateContributors
    swift package regenerate-contributors-list

    View Slide

  62. View Slide

  63. COMMAND PLUGINS
    ▸ Can ask permission to write in the package directory
    ▸ Can be invoked at the command line
    ▸ Can be invoked from a menu item in Xcode

    View Slide

  64. View Slide

  65. View Slide

  66. View Slide

  67. View Slide

  68. View Slide

  69. COMMAND PLUGINS
    ▸ Can ask permission to write in the package directory
    ▸ Can be invoked at the command line
    ▸ Can be invoked from a menu item Xcode
    ▸ Either way, must be invoked separately from the build process

    View Slide

  70. BUILD TOOL PLUGINS

    View Slide

  71. BUILD TOOL PLUGINS
    RUN AUTOMATICALLY ON EVERY BUILD

    View Slide

  72. BUILD TOOL PLUGINS
    (THE THING I REALLY WANT TO
    USE)

    View Slide

  73. THE MORE COMPLICATED BUT LESS COMPLICATED WAY:
    EMBED A PACKAGE
    IN YOUR .XCODEPROJ

    View Slide

  74. MORE COMPLICATED
    (YOU HAVE TO ADD A WHOLE SWIFT PACKAGE TO YOUR PROJECT)

    View Slide

  75. LESS COMPLICATED
    (IT WORKS WAY MORE RELIABLY)

    View Slide

  76. SAMPLE APP
    https://github.com/designatednerd/
    RadioactiveCat

    View Slide

  77. via https://energyeducation.ca/encyclopedia/Half_life

    View Slide

  78. View Slide

  79. View Slide

  80. View Slide

  81. BUILD TOOL EASY MODE
    BRING IN SOMEONE ELSE'S PLUGIN

    View Slide

  82. DO NIL DISTURB
    HTTPS://GITHUB.COM/ICANZILB/
    DONILDISTURBPLUGIN

    View Slide

  83. // Package.swift
    .dependencies: [
    .package(url: "https://github.com/icanzilb/
    DoNilDisturbPlugin.git", from: "0.0.5"),
    ]

    View Slide

  84. // Package.swift
    .target(
    name: "Cats",
    dependencies: [
    ],
    plugins: [
    .plugin(name: "DoNilDisturbPlugin",
    package: "DoNilDisturbPlugin")
    ]
    ),

    View Slide

  85. View Slide

  86. DO NOT BLINDLY CLICK
    TRUST & ENABLE ALL

    View Slide

  87. View Slide

  88. View Slide

  89. View Slide

  90. View Slide

  91. View Slide

  92. BUILD TOOL HARD MODE:
    CREATE YOUR OWN PLUGIN

    View Slide

  93. DON'T BUILD YOUR PLUGIN
    AND THE PACKAGE YOU'RE TESTING IT AGAINST
    IN THE SAME REPO

    View Slide

  94. GITHUB.COM/DESIGNATEDNERD/
    DATEDIMAGEGENERATOR

    View Slide

  95. PLUGIN VS. EXECUTABLE

    View Slide

  96. // File structure of the Swift Lib
    !
    DatedImageGenerator
    !
    Plugins
    !
    DatedImageGenerator
    "
    DatedImageGenerator.swift
    !
    Sources
    !
    DatedImage
    "
    DatedImage.swift
    "
    ExifDateFormatter.swift
    !
    DatedImageGeneratorExecutable
    "
    DatedImageGeneratorExecutable.swift
    !
    Tests
    !
    DatedImageGeneratorTests
    "
    DatedImageGeneratorExecutable.swift
    "
    Media.xcassets

    View Slide

  97. // File structure of the Swift Lib
    !
    DatedImageGenerator
    !
    Plugins
    !
    DatedImageGenerator
    "
    DatedImageGenerator.swift
    !
    Sources
    !
    DatedImage
    "
    DatedImage.swift
    "
    ExifDateFormatter.swift
    !
    DatedImageGeneratorExecutable
    "
    DatedImageGeneratorExecutable.swift
    !
    Tests
    !
    DatedImageGeneratorTests
    "
    DatedImageGeneratorExecutable.swift
    "
    Media.xcassets

    View Slide

  98. // File structure of the Swift Lib
    !
    DatedImageGenerator
    !
    Plugins
    !
    DatedImageGenerator
    "
    DatedImageGenerator.swift
    !
    Sources
    !
    DatedImage
    "
    DatedImage.swift
    "
    ExifDateFormatter.swift
    !
    DatedImageGeneratorExecutable
    "
    DatedImageGeneratorExecutable.swift
    !
    Tests
    !
    DatedImageGeneratorTests
    "
    DatedImageGeneratorExecutable.swift
    "
    Media.xcassets

    View Slide

  99. // File structure of the Swift Lib
    !
    DatedImageGenerator
    !
    Plugins
    !
    DatedImageGenerator
    "
    DatedImageGenerator.swift
    !
    Sources
    !
    DatedImage
    "
    DatedImage.swift
    "
    ExifDateFormatter.swift
    !
    DatedImageGeneratorExecutable
    "
    DatedImageGeneratorExecutable.swift
    !
    Tests
    !
    DatedImageGeneratorTests
    "
    DatedImageGeneratorExecutable.swift
    "
    Media.xcassets

    View Slide

  100. // File structure of the Swift Lib
    !
    DatedImageGenerator
    !
    Plugins
    !
    DatedImageGenerator
    "
    DatedImageGenerator.swift
    !
    Sources
    !
    DatedImage // <-- Exported library used in executable and main app
    "
    DatedImage.swift
    "
    ExifDateFormatter.swift
    !
    DatedImageGeneratorExecutable
    "
    DatedImageGeneratorExecutable.swift
    !
    Tests
    !
    DatedImageGeneratorTests
    "
    DatedImageGeneratorExecutable.swift
    "
    Media.xcassets

    View Slide

  101. // File structure of the Swift Lib
    !
    DatedImageGenerator
    !
    Plugins
    !
    DatedImageGenerator
    "
    DatedImageGenerator.swift
    !
    Sources
    !
    DatedImage // <-- Exported Framework used in executable and main app
    "
    DatedImage.swift
    "
    ExifDateFormatter.swift
    !
    DatedImageGeneratorExecutable // <-- The thing that does all the work
    "
    DatedImageGeneratorExecutable.swift
    !
    Tests
    !
    DatedImageGeneratorTests
    "
    DatedImageGeneratorExecutable.swift
    "
    Media.xcassets

    View Slide

  102. // File structure of the Swift Lib
    !
    DatedImageGenerator
    !
    Plugins
    !
    DatedImageGenerator
    "
    DatedImageGenerator.swift
    !
    Sources
    !
    DatedImage
    "
    DatedImage.swift
    "
    ExifDateFormatter.swift
    !
    DatedImageGeneratorExecutable
    "
    DatedImageGeneratorExecutable.swift
    !
    Tests
    !
    DatedImageGeneratorTests
    "
    DatedImageGeneratorExecutable.swift
    "
    Media.xcassets

    View Slide

  103. // Plugins/DatedImageGenerator.swift
    @main
    struct DatedImageGenerator: BuildToolPlugin {
    func createBuildCommands(context: PluginContext,
    target: Target) async throws -> [Command] {
    // TODO: Create info for build command
    return [
    .buildCommand(
    displayName: "Generate Dated Images",
    executable: try context.tool(named: "DatedImageGeneratorExecutable").path,
    // more to come here
    arguments: "TODO",
    outputFiles: ["TODO"]
    )
    ]
    }
    }

    View Slide

  104. WHAT'S IN THE
    EXECUTABLE?

    View Slide

  105. // Sources/DatedImageGeneratorExecutable/
    // DatedImageGeneratorExecutable.swift
    @main
    struct DatedImageGeneratorExecutable {
    static func main() throws {
    // How do we get information from the Plugin?
    }
    }

    View Slide

  106. // Sources/DatedImageGeneratorExecutable/
    // DatedImageGeneratorExecutable.swift
    @main
    struct DatedImageGeneratorExecutable {
    static func main() throws {
    // Grab the 2nd argument as a string
    ProcessInfo.processInfo.arguments[1]
    }
    }

    View Slide

  107. // Sources/DatedImageGeneratorExecutable/
    // DatedImageGeneratorExecutable.swift
    @main
    struct DatedImageGeneratorExecutable {
    static func main() throws {
    // Turn it into Data
    Data(ProcessInfo.processInfo.arguments[1].utf8))
    }
    }

    View Slide

  108. // Sources/DatedImageGeneratorExecutable/
    // DatedImageGeneratorExecutable.swift
    @main
    struct DatedImageGeneratorExecutable {
    static func main() throws {
    // Decode it into a codable object!
    let invocation = try JSONDecoder()
    .decode(PluginInvocation.self,
    from: Data(ProcessInfo.processInfo.arguments[1].utf8))
    }
    }

    View Slide

  109. "I should just put the codable object in
    the executable and import it into the
    plugin"

    View Slide

  110. View Slide

  111. View Slide

  112. View Slide

  113. // One copy of this in the plugin, one in the executable
    struct PluginInvocation: Codable {
    let catalogPaths: [String]
    let outputPath: String
    func encodedString() throws -> String {
    let data = try JSONEncoder().encode(self)
    return String(decoding: data, as: UTF8.self)
    }
    }

    View Slide

  114. // One copy of this in the plugin, one in the executable
    // (or share with a common lib)
    struct PluginInvocation: Codable {
    let catalogPaths: [String]
    let outputPath: String
    func encodedString() throws -> String {
    let data = try JSONEncoder().encode(self)
    return String(decoding: data, as: UTF8.self)
    }
    }

    View Slide

  115. // Plugins/DatedImageGenerator.swift
    @main
    struct DatedImageGenerator: BuildToolPlugin {
    func createBuildCommands(context: PluginContext,
    target: Target) async throws -> [Command] {
    guard let target = target as? SourceModuleTarget else { throw ImageGenError.notASourceModule }
    let assetCatalogPaths = target.sourceFiles(withSuffix: "xcassets")
    .compactMap { assetCatalog in
    return assetCatalog.path.string
    }
    let outputPath = context.pluginWorkDirectory
    .appending(["DatedImages.swift"])
    let invocation = PluginInvocation(catalogPaths: assetCatalogPaths,
    outputPath: outputPath.string)
    return [
    .buildCommand(
    displayName: "Generate Dated Images",
    executable: try context.tool(named: "DatedImageGeneratorExecutable").path,
    arguments: [try invocation.encodedString()],
    outputFiles: [outputPath]
    )
    ]
    }
    }

    View Slide

  116. // Plugins/DatedImageGenerator.swift
    @main
    struct DatedImageGenerator: BuildToolPlugin {
    func createBuildCommands(context: PluginContext,
    target: Target) async throws -> [Command] {
    guard let target = target as? SourceModuleTarget else { throw ImageGenError.notASourceModule }
    let assetCatalogPaths = target.sourceFiles(withSuffix: "xcassets")
    .compactMap { assetCatalog in
    return assetCatalog.path.string
    }
    let outputPath = context.pluginWorkDirectory
    .appending(["DatedImages.swift"])
    let invocation = PluginInvocation(catalogPaths: assetCatalogPaths,
    outputPath: outputPath.string)
    return [
    .buildCommand(
    displayName: "Generate Dated Images",
    executable: try context.tool(named: "DatedImageGeneratorExecutable").path,
    arguments: [try invocation.encodedString()],
    outputFiles: [outputPath]
    )
    ]
    }
    }

    View Slide

  117. !
    TARGET
    (TECHNICALLY PACKAGEPLUGIN.TARGET)

    View Slide

  118. // Plugins/DatedImageGenerator.swift
    @main
    struct DatedImageGenerator: BuildToolPlugin {
    func createBuildCommands(context: PluginContext,
    target: Target) async throws -> [Command] {
    guard let target = target as? SourceModuleTarget else { throw ImageGenError.notASourceModule }
    let assetCatalogPaths = target.sourceFiles(withSuffix: "xcassets")
    .compactMap { assetCatalog in
    return assetCatalog.path.string
    }
    let outputPath = context.pluginWorkDirectory
    .appending(["DatedImages.swift"])
    let invocation = PluginInvocation(catalogPaths: assetCatalogPaths,
    outputPath: outputPath.string)
    return [
    .buildCommand(
    displayName: "Generate Dated Images",
    executable: try context.tool(named: "DatedImageGeneratorExecutable").path,
    arguments: [try invocation.encodedString()],
    outputFiles: [outputPath]
    )
    ]
    }
    }

    View Slide

  119. !
    PLUGIN CONTEXT
    (TECHNICALLY PACKAGEPLUGIN.PLUGINCONTEXT)

    View Slide

  120. PLUGINWORKDIRECTORY
    IS YOUR SANDBOX

    View Slide

  121. // Plugins/DatedImageGenerator.swift
    @main
    struct DatedImageGenerator: BuildToolPlugin {
    func createBuildCommands(context: PluginContext,
    target: Target) async throws -> [Command] {
    guard let target = target as? SourceModuleTarget else { throw ImageGenError.notASourceModule }
    let assetCatalogPaths = target.sourceFiles(withSuffix: "xcassets")
    .compactMap { assetCatalog in
    return assetCatalog.path.string
    }
    let outputPath = context.pluginWorkDirectory
    .appending(["DatedImages.swift"])
    let invocation = PluginInvocation(catalogPaths: assetCatalogPaths,
    outputPath: outputPath.string)
    return [
    .buildCommand(
    displayName: "Generate Dated Images",
    executable: try context.tool(named: "DatedImageGeneratorExecutable").path,
    arguments: [try invocation.encodedString()],
    outputFiles: [outputPath]
    )
    ]
    }
    }

    View Slide

  122. // Plugins/DatedImageGenerator.swift
    @main
    struct DatedImageGenerator: BuildToolPlugin {
    func createBuildCommands(context: PluginContext,
    target: Target) async throws -> [Command] {
    guard let target = target as? SourceModuleTarget else { throw ImageGenError.notASourceModule }
    let assetCatalogPaths = target.sourceFiles(withSuffix: "xcassets")
    .compactMap { assetCatalog in
    return assetCatalog.path.string
    }
    let outputPath = context.pluginWorkDirectory
    .appending(["DatedImages.swift"])
    let invocation = PluginInvocation(catalogPaths: assetCatalogPaths,
    outputPath: outputPath.string)
    return [
    .buildCommand(
    displayName: "Generate Dated Images",
    executable: try context.tool(named: "DatedImageGeneratorExecutable").path,
    arguments: [try invocation.encodedString()],
    outputFiles: [outputPath]
    )
    ]
    }
    }

    View Slide

  123. View Slide

  124. QUICK RECAP OF PLUGIN CREATION

    View Slide

  125. QUICK RECAP OF PLUGIN CREATION
    ▸ Create a plugin in Plugins

    View Slide

  126. QUICK RECAP OF PLUGIN CREATION
    ▸ Create a plugin in Plugins
    ▸ Create an executable it will call in Sources

    View Slide

  127. QUICK RECAP OF PLUGIN CREATION
    ▸ Create a plugin in Plugins
    ▸ Create an executable it will call in Sources
    ▸ Use a codable object to pass data between them

    View Slide

  128. QUICK RECAP OF PLUGIN CREATION
    ▸ Create a plugin in Plugins
    ▸ Create an executable it will call in Sources
    ▸ Use a codable object to pass data between them
    ▸ Note that the plugin depends on but cannot import the executable

    View Slide

  129. !
    BACK TO THE KITTAHS!

    View Slide

  130. // In Cats/Package.swift
    dependencies: [
    .package(url: "https://github.com/designatednerd/
    DatedImageGenerator.git", from: "0.0.1"),
    .package(url: "https://github.com/icanzilb/
    DoNilDisturbPlugin.git", from: "0.0.5"),
    ],

    View Slide

  131. // In Cats/Package.swift
    .target(
    name: "Cats",
    dependencies: [
    .product(name: "DatedImage", package: "DatedImageGenerator"),
    ],
    plugins: [
    .plugin(name: "DatedImageGenerator", package: "DatedImageGenerator"),
    .plugin(name: "DoNilDisturbPlugin", package: "DoNilDisturbPlugin")
    ]
    ),

    View Slide

  132. // In Cats/Package.swift
    .target(
    name: "Cats",
    dependencies: [
    .product(name: "DatedImage", package: "DatedImageGenerator"),
    ],
    plugins: [
    .plugin(name: "DatedImageGenerator", package: "DatedImageGenerator"),
    .plugin(name: "DoNilDisturbPlugin", package: "DoNilDisturbPlugin")
    ]
    ),

    View Slide

  133. // In Cats/Package.swift
    .target(
    name: "Cats",
    dependencies: [
    .product(name: "DatedImage", package: "DatedImageGenerator"),
    ],
    plugins: [
    .plugin(name: "DatedImageGenerator", package: "DatedImageGenerator"),
    .plugin(name: "DoNilDisturbPlugin", package: "DoNilDisturbPlugin")
    ]
    ),

    View Slide

  134. View Slide

  135. View Slide

  136. View Slide

  137. View Slide

  138. View Slide

  139. View Slide

  140. WHAT ABOUT RUNNING PLUGINS
    ON AN APP TARGET?

    View Slide

  141. XCODE PROJECT PLUGIN

    View Slide

  142. XCODE PROJECT PLUGIN
    (A KIND OF BUILD TOOL PLUGIN)

    View Slide

  143. #if canImport(XcodeProjectPlugin)
    import XcodeProjectPlugin
    extension DatedImageGenerator: XcodeBuildToolPlugin {
    func createBuildCommands(context: XcodePluginContext,
    target: XcodeTarget) throws -> [Command] {
    let assetCatalogPaths = context.xcodeProject.filePaths
    .filter({ $0.string.hasSuffix("xcassets")})
    .map { $0.string }
    // Rest of the code the same as in the main plugin
    }
    }
    #endif

    View Slide

  144. #if canImport(XcodeProjectPlugin)
    import XcodeProjectPlugin
    extension DatedImageGenerator: XcodeBuildToolPlugin {
    func createBuildCommands(context: XcodePluginContext,
    target: XcodeTarget) throws -> [Command] {
    let assetCatalogPaths = context.xcodeProject.filePaths
    .filter({ $0.string.hasSuffix("xcassets")})
    .map { $0.string }
    // Rest of the code the same as in the main plugin
    }
    }
    #endif

    View Slide

  145. #if canImport(XcodeProjectPlugin)
    import XcodeProjectPlugin
    extension DatedImageGenerator: XcodeBuildToolPlugin {
    func createBuildCommands(context: XcodePluginContext,
    target: XcodeTarget) throws -> [Command] {
    let assetCatalogPaths = context.xcodeProject.filePaths
    .filter({ $0.string.hasSuffix("xcassets")})
    .map { $0.string }
    // Rest of the code the same as in the main plugin
    }
    }
    #endif

    View Slide

  146. #if canImport(XcodeProjectPlugin)
    import XcodeProjectPlugin
    extension DatedImageGenerator: XcodeBuildToolPlugin {
    func createBuildCommands(context: XcodePluginContext,
    target: XcodeTarget) throws -> [Command] {
    let assetCatalogPaths = context.xcodeProject.filePaths
    .filter({ $0.string.hasSuffix("xcassets")})
    .map { $0.string }
    // Rest of the code the same as in the main plugin
    }
    }
    #endif

    View Slide

  147. #if canImport(XcodeProjectPlugin)
    import XcodeProjectPlugin
    extension DatedImageGenerator: XcodeBuildToolPlugin {
    func createBuildCommands(context: XcodePluginContext,
    target: XcodeTarget) throws -> [Command] {
    let assetCatalogPaths = context.xcodeProject.filePaths
    .filter({ $0.string.hasSuffix("xcassets")})
    .map { $0.string }
    // Rest of the code the same as in the main plugin
    }
    }
    #endif

    View Slide

  148. View Slide

  149. View Slide

  150. View Slide

  151. View Slide

  152. View Slide

  153. View Slide

  154. SPM PLUGIN LIMITATIONS

    View Slide

  155. SPM PLUGIN LIMITATIONS
    (that I've found so far)

    View Slide

  156. SPM PLUGIN LIMITATIONS
    (that I've found so far)
    ▸ Still no UI for you

    View Slide

  157. SPM PLUGIN LIMITATIONS
    (that I've found so far)
    ▸ Still no UI for you
    ▸ You can't even ask to use the network

    View Slide

  158. SPM PLUGIN LIMITATIONS
    (that I've found so far)
    ▸ Still no UI for you
    ▸ You can't even ask to use the network
    ▸ You can't share data between plugins

    View Slide

  159. SPM PLUGIN LIMITATIONS
    (that I've found so far)
    ▸ Still no UI for you
    ▸ You can't even ask to use the network
    ▸ You can't share data between plugins
    ▸ Can't do anything with build results

    View Slide

  160. !
    PLUGIN WISH LIST

    View Slide

  161. PLUGIN WISH LIST
    ▸ File metadata

    View Slide

  162. PLUGIN WISH LIST
    ▸ File metadata
    ▸ Ability to show UI in Xcode

    View Slide

  163. OBLIGATORY SUMMARY SLIDE

    View Slide

  164. OBLIGATORY SUMMARY SLIDE
    ▸ Alcatraz

    View Slide

  165. OBLIGATORY SUMMARY SLIDE
    ▸ Alcatraz
    ! "
    Xcode 8 Extensions

    View Slide

  166. OBLIGATORY SUMMARY SLIDE
    ▸ Alcatraz
    ! "
    Xcode 8 Extensions
    ▸ SPM Plugins to the rescue!

    View Slide

  167. OBLIGATORY SUMMARY SLIDE
    ▸ Alcatraz
    ! "
    Xcode 8 Extensions
    ▸ SPM Plugins to the rescue!
    ▸ Command plugins: On demand, Build tool plugins: On every build

    View Slide

  168. OBLIGATORY SUMMARY SLIDE
    ▸ Alcatraz
    ! "
    Xcode 8 Extensions
    ▸ SPM Plugins to the rescue!
    ▸ Command plugins: On demand, Build tool plugins: On every build
    ▸ Security: Be super-mindful of which plugins you use

    View Slide

  169. OBLIGATORY SUMMARY SLIDE
    ▸ Alcatraz
    ! "
    Xcode 8 Extensions
    ▸ SPM Plugins to the rescue!
    ▸ Command plugins: On demand, Build tool plugins: On every build
    ▸ Security: Be super-mindful of which plugins you use
    ▸ Building plugins is a bit complicated, but also pretty fun

    View Slide

  170. THANK YOU!

    View Slide

  171. SWIFT-EVOLUTION LINKS!
    ▸ SE-0303: Extensible Build Tools github.com/apple/swift-evolution/
    blob/main/proposals/0303-swiftpm-extensible-build-tools.md
    ▸ SE-0325: Additional Plugin APIs github.com/apple/swift-evolution/
    blob/main/proposals/0325-swiftpm-additional-plugin-apis.md
    ▸ SE-0332: SPM Command Plugins github.com/apple/swift-evolution/
    blob/main/proposals/0332-swiftpm-command-plugins.md

    View Slide

  172. !
    SLIGHTLY TERRIFYING LINKS!
    ▸ Xcode Ghost details: en.wikipedia.org/wiki/XcodeGhost
    ▸ Strawhorse leaked document: theintercept.com/document/
    2015/03/10/strawhorse-attacking-macos-ios-software-
    development-kit
    ▸ Strawhorse Context: theintercept.com/2015/03/10/ispy-cia-
    campaign-steal-apples-secrets/

    View Slide

  173. !
    PLUGIN LINKS!
    ▸ Do Nil Disturb: github.com/icanzilb/DoNilDisturbPlugin
    ▸ Swift SafeURL: github.com/baguio/SwiftSafeURL

    View Slide