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

[iOS Dev UK 23] Making developer tools with Swift

[iOS Dev UK 23] Making developer tools with Swift

Pol Piella Abadia

September 05, 2023
Tweet

More Decks by Pol Piella Abadia

Other Decks in Programming

Transcript

  1. Making developer
    tools with Swift
    Pol Piella Abadia


    Senior iOS Developer, BBC iPlayer
    iOS Dev UK · Aberystwyth, Wales


    10:20


    Great Hall

    View full-size slide

  2. Senior software engineer based in Manchester
    Hi! I’m Pol

    View full-size slide

  3. Content creation
    Apps I am working on
    Public speaking

    View full-size slide

  4. Content creation
    Apps I am working on
    Public speaking

    View full-size slide

  5. Content creation
    Apps I am working on
    Public speaking

    View full-size slide

  6. Swift &
    Developer
    Tools?

    View full-size slide

  7. Swift &
    Developer
    Tools!

    View full-size slide

  8. You know
    Swift!

    View full-size slide

  9. You (and your
    team) know
    Swift!

    View full-size slide

  10. Focus on the
    system and NOT
    the language!

    View full-size slide

  11. You (and your
    audience)
    know Swift!

    View full-size slide

  12. Can you build
    it with Swift?

    View full-size slide

  13. #1 A simple
    CLI script?

    View full-size slide

  14. feature/MYTICKET-123_name-of-your-branch

    View full-size slide

  15. feature/MYTICKET-123_name-of-your-branch
    Author: Pol Piella Abadia


    Date: Sun Aug 20 14:03:41 2023 +0100


    My awesome feature


    TICKET-123
    Branch
    Commit

    View full-size slide

  16. # !
    /usr/bin/env swift


    import Foundation


    let commitMessageFile = CommandLine.arguments[1]


    guard let data = FileManager.default.contents(atPath: commitMessageFile),


    let commitMessage = String(data: data, encoding: .utf8) else {


    exit(1)


    }


    let gitBranchName = shell("git rev-parse
    --
    abbrev-ref HEAD")


    .trimmingCharacters(in: .newlines)


    let regex = try! NSRegularExpression(pattern: #"(\w+-\d+)"#, options: .anchorsMatchLines)


    let stringRange = NSRange(location: 0, length: gitBranchName.utf16.count)


    guard let match = regex.firstMatch(in: gitBranchName, range: stringRange) else { exit(0) }


    let range = match.range(at: 1)


    guard !NSEqualRanges(range, NSMakeRange(NSNotFound, 0)) else { exit(1) }


    let ticketNumber = (gitBranchName as NSString)


    .substring(with: range)


    .trimmingCharacters(in: .newlines)


    if !commitMessage.contains(ticketNumber) {


    do {


    try "\(commitMessage.trimmingCharacters(in:.newlines))\n\n\(ticketNumber)"


    .write(toFile: commitMessageFile, atomically: true, encoding: .utf8)


    } catch {


    print("Could not write to file \(commitMessageFile)")


    }


    }

    View full-size slide

  17. # !
    /usr/bin/env swift


    import Foundation


    let commitMessageFile = CommandLine.arguments[1]


    guard let data = FileManager.default.contents(atPath: commitMessageFile),


    let commitMessage = String(data: data, encoding: .utf8) else {


    exit(1)


    }


    let gitBranchName = shell("git rev-parse
    --
    abbrev-ref HEAD")


    .trimmingCharacters(in: .newlines)


    let regex = try! NSRegularExpression(pattern: #"(\w+-\d+)"#, options: .anchorsMatchLines)


    let stringRange = NSRange(location: 0, length: gitBranchName.utf16.count)


    guard let match = regex.firstMatch(in: gitBranchName, range: stringRange) else { exit(0) }


    let range = match.range(at: 1)


    guard !NSEqualRanges(range, NSMakeRange(NSNotFound, 0)) else { exit(1) }


    let ticketNumber = (gitBranchName as NSString)


    .substring(with: range)


    .trimmingCharacters(in: .newlines)


    if !commitMessage.contains(ticketNumber) {


    do {


    try "\(commitMessage.trimmingCharacters(in:.newlines))\n\n\(ticketNumber)"


    .write(toFile: commitMessageFile, atomically: true, encoding: .utf8)


    } catch {


    print("Could not write to file \(commitMessageFile)")


    }


    }

    View full-size slide

  18. chmod +x file_where_hook_code_is

    View full-size slide

  19. Not so fast…

    View full-size slide

  20. Yes and no…
    https://github.com/mxcl/swift-sh

    View full-size slide

  21. # !
    /usr/bin/env swift-sh


    import Foundation


    let commitMessageFile = CommandLine.arguments[1]


    guard let data = FileManager.default.contents(atPath: commitMessageFile),


    let commitMessage = String(data: data, encoding: .utf8) else {


    exit(1)


    }


    let gitBranchName = shell("git rev-parse
    --
    abbrev-ref HEAD")


    .trimmingCharacters(in: .newlines)


    let regex = try! NSRegularExpression(pattern: #"(\w+-\d+)"#, options: .anchorsMatchLines)


    let stringRange = NSRange(location: 0, length: gitBranchName.utf16.count)


    guard let match = regex.firstMatch(in: gitBranchName, range: stringRange) else { exit(0) }


    let range = match.range(at: 1)


    guard !NSEqualRanges(range, NSMakeRange(NSNotFound, 0)) else { exit(1) }


    let ticketNumber = (gitBranchName as NSString)


    .substring(with: range)


    .trimmingCharacters(in: .newlines)


    if !commitMessage.contains(ticketNumber) {


    do {


    try "\(commitMessage.trimmingCharacters(in:.newlines))\n\n\(ticketNumber)"


    .write(toFile: commitMessageFile, atomically: true, encoding: .utf8)


    } catch {


    print("Could not write to file \(commitMessageFile)")


    }


    }

    View full-size slide

  22. # !
    /usr/bin/env swift-sh


    import Foundation


    import SourceKittenFramework
    //
    @jpsim
    ~>
    0.34.1


    let commitMessageFile = CommandLine.arguments[1]


    guard let data = FileManager.default.contents(atPath: commitMessageFile),


    let commitMessage = String(data: data, encoding: .utf8) else {


    exit(1)


    }


    let gitBranchName = shell("git rev-parse
    --
    abbrev-ref HEAD")


    .trimmingCharacters(in: .newlines)


    let regex = try! NSRegularExpression(pattern: #"(\w+-\d+)"#, options: .anchorsMatchLines)


    let stringRange = NSRange(location: 0, length: gitBranchName.utf16.count)


    guard let match = regex.firstMatch(in: gitBranchName, range: stringRange) else { exit(0) }


    let range = match.range(at: 1)


    guard !NSEqualRanges(range, NSMakeRange(NSNotFound, 0)) else { exit(1) }


    let ticketNumber = (gitBranchName as NSString)


    .substring(with: range)


    .trimmingCharacters(in: .newlines)


    if !commitMessage.contains(ticketNumber) {


    do {


    try "\(commitMessage.trimmingCharacters(in:.newlines))\n\n\(ticketNumber)"


    .write(toFile: commitMessageFile, atomically: true, encoding: .utf8)


    } catch {


    print("Could not write to file \(commitMessageFile)")


    }


    }

    View full-size slide

  23. Future-
    proofing and
    scaling

    View full-size slide

  24. swift package init
    --
    type executable

    View full-size slide

  25. //
    swift-tools-version: 5.9


    import PackageDescription


    let package = Package(


    name: "GitHookExecutable",


    dependencies: [


    ],


    targets: [


    .executableTarget(


    name: "GitHookExecutable",


    dependencies: [


    ]


    ),


    ]


    )

    View full-size slide

  26. //
    swift-tools-version: 5.9


    import PackageDescription


    let package = Package(


    name: "GitHookExecutable",


    dependencies: [


    .package(url: "https:
    //
    github.com/apple/swift-argument-parser.git", exact: "1.2.3")


    ],


    targets: [


    .executableTarget(


    name: "GitHookExecutable",


    dependencies: [


    .product(name: "ArgumentParser", package: "swift-argument-parser")


    ]


    ),


    ]


    )

    View full-size slide

  27. import ArgumentParser


    @main


    struct GitHookExecutable: ParsableCommand {


    @Argument(help: "The path to the commit message file")


    var commitMessagePath: String


    func run() throws {


    let commitMessage = try String(contentsOfFile: commitMessagePath)


    // .
    ..


    }


    }


    View full-size slide

  28. Learn more about it!
    https://www.youtube.com/watch?v=yyOXynCoagE&t=611s

    View full-size slide

  29. #2 Running a
    build script?

    View full-size slide

  30. brew install swiftlint
    pod install

    View full-size slide

  31. What about
    Swift
    Packages?

    View full-size slide

  32. Swift 5.6 to the rescue!
    https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md

    View full-size slide

  33. //
    swift-tools-version:5.7


    import PackageDescription


    let package = Package(


    name: "SwiftLint",


    platforms: [.macOS(.v12)],


    products: [


    .plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])


    ],


    dependencies: [


    / / .. .

    ],


    targets: [


    .plugin(


    name: "SwiftLintPlugin",


    capability: .buildTool(),


    dependencies: [


    .target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),


    .target(name: "swiftlint", condition: .when(platforms: [.linux]))


    ]


    ),


    .executableTarget(


    name: "swiftlint",


    dependencies: [


    .product(name: "ArgumentParser", package: "swift-argument-parser"),


    "CollectionConcurrencyKit",


    "SwiftLintFramework",


    "SwiftyTextTable",


    ]


    ),


    .binaryTarget(


    name: "SwiftLintBinary",


    url: "https:
    //
    github.com/realm/SwiftLint/releases/download/0.52.4/SwiftLintBinary-macos.artifactbundle.zip",


    checksum: "8a8095e6235a07d00f34a9e500e7568b359f6f66a249f36d12cd846017a8c6f5"


    )


    ]


    )
    https://github.com/realm/SwiftLint/blob/main/Package.swift

    View full-size slide

  34. //
    swift-tools-version:5.7


    import PackageDescription


    let package = Package(


    name: "SwiftLint",


    platforms: [.macOS(.v12)],


    products: [


    .plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])


    ],


    dependencies: [


    / / .. .

    ],


    targets: [


    .plugin(


    name: "SwiftLintPlugin",


    capability: .buildTool(),


    dependencies: [


    .target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),


    .target(name: "swiftlint", condition: .when(platforms: [.linux]))


    ]


    ),


    .executableTarget(


    name: "swiftlint",


    dependencies: [


    .product(name: "ArgumentParser", package: "swift-argument-parser"),


    "CollectionConcurrencyKit",


    "SwiftLintFramework",


    "SwiftyTextTable",


    ]


    ),


    .binaryTarget(


    name: "SwiftLintBinary",


    url: "https:
    //
    github.com/realm/SwiftLint/releases/download/0.52.4/SwiftLintBinary-macos.artifactbundle.zip",


    checksum: "8a8095e6235a07d00f34a9e500e7568b359f6f66a249f36d12cd846017a8c6f5"


    )


    ]


    )
    https://github.com/realm/SwiftLint/blob/main/Package.swift

    View full-size slide

  35. //
    swift-tools-version:5.7


    import PackageDescription


    let package = Package(


    name: "SwiftLint",


    platforms: [.macOS(.v12)],


    products: [


    .plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])


    ],


    dependencies: [


    / / .. .

    ],


    targets: [


    .plugin(


    name: "SwiftLintPlugin",


    capability: .buildTool(),


    dependencies: [


    .target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),


    .target(name: "swiftlint", condition: .when(platforms: [.linux]))


    ]


    ),


    .executableTarget(


    name: "swiftlint",


    dependencies: [


    .product(name: "ArgumentParser", package: "swift-argument-parser"),


    "CollectionConcurrencyKit",


    "SwiftLintFramework",


    "SwiftyTextTable",


    ]


    ),


    .binaryTarget(


    name: "SwiftLintBinary",


    url: "https:
    //
    github.com/realm/SwiftLint/releases/download/0.52.4/SwiftLintBinary-macos.artifactbundle.zip",


    checksum: "8a8095e6235a07d00f34a9e500e7568b359f6f66a249f36d12cd846017a8c6f5"


    )


    ]


    )
    https://github.com/realm/SwiftLint/blob/main/Package.swift

    View full-size slide

  36. //
    swift-tools-version:5.7


    import PackageDescription


    let package = Package(


    name: "SwiftLint",


    platforms: [.macOS(.v12)],


    products: [


    .plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])


    ],


    dependencies: [


    / / .. .

    ],


    targets: [


    .plugin(


    name: "SwiftLintPlugin",


    capability: .buildTool(),


    dependencies: [


    .target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),


    .target(name: "swiftlint", condition: .when(platforms: [.linux]))


    ]


    ),


    .executableTarget(


    name: "swiftlint",


    dependencies: [


    .product(name: "ArgumentParser", package: "swift-argument-parser"),


    "CollectionConcurrencyKit",


    "SwiftLintFramework",


    "SwiftyTextTable",


    ]


    ),


    .binaryTarget(


    name: "SwiftLintBinary",


    url: "https:
    //
    github.com/realm/SwiftLint/releases/download/0.52.4/SwiftLintBinary-macos.artifactbundle.zip",


    checksum: "8a8095e6235a07d00f34a9e500e7568b359f6f66a249f36d12cd846017a8c6f5"


    )


    ]


    )
    https://github.com/realm/SwiftLint/blob/main/Package.swift

    View full-size slide

  37. //
    swift-tools-version:5.7


    import PackageDescription


    let package = Package(


    name: "SwiftLint",


    platforms: [.macOS(.v12)],


    products: [


    .plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])


    ],


    dependencies: [


    / / .. .

    ],


    targets: [


    .plugin(


    name: "SwiftLintPlugin",


    capability: .buildTool(),


    dependencies: [


    .target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),


    .target(name: "swiftlint", condition: .when(platforms: [.linux]))


    ]


    ),


    .executableTarget(


    name: "swiftlint",


    dependencies: [


    .product(name: "ArgumentParser", package: "swift-argument-parser"),


    "CollectionConcurrencyKit",


    "SwiftLintFramework",


    "SwiftyTextTable",


    ]


    ),


    .binaryTarget(


    name: "SwiftLintBinary",


    url: "https:
    //
    github.com/realm/SwiftLint/releases/download/0.52.4/SwiftLintBinary-macos.artifactbundle.zip",


    checksum: "8a8095e6235a07d00f34a9e500e7568b359f6f66a249f36d12cd846017a8c6f5"


    )


    ]


    )
    https://github.com/realm/SwiftLint/blob/main/Package.swift

    View full-size slide

  38. import Foundation


    import PackagePlugin


    @main


    struct SwiftLintPlugin: BuildToolPlugin {


    func createBuildCommands(context: PluginContext, target: Target) async throws
    -
    >
    [Command] {


    guard let sourceTarget = target as? SourceModuleTarget else {


    return []


    }


    return createBuildCommands(


    inputFiles: sourceTarget.sourceFiles(withSuffix: "swift").map(\.path),


    packageDirectory: context.package.directory,


    workingDirectory: context.pluginWorkDirectory,


    tool: try context.tool(named: "swiftlint")


    )


    }


    private func createBuildCommands(


    inputFiles: [Path],


    packageDirectory: Path,


    workingDirectory: Path,


    tool: PluginContext.Tool


    )
    ->
    [Command] {


    // . ..

    return [


    .prebuildCommand(


    displayName: "SwiftLint",


    executable: tool.path,


    arguments: arguments,


    outputFilesDirectory: outputFilesDirectory


    )


    ]


    }


    }


    https://github.com/realm/SwiftLint/blob/main/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift

    View full-size slide

  39. import Foundation


    import PackagePlugin


    @main


    struct SwiftLintPlugin: BuildToolPlugin {


    func createBuildCommands(context: PluginContext, target: Target) async throws
    -
    >
    [Command] {


    guard let sourceTarget = target as? SourceModuleTarget else {


    return []


    }


    return createBuildCommands(


    inputFiles: sourceTarget.sourceFiles(withSuffix: "swift").map(\.path),


    packageDirectory: context.package.directory,


    workingDirectory: context.pluginWorkDirectory,


    tool: try context.tool(named: "swiftlint")


    )


    }


    private func createBuildCommands(


    inputFiles: [Path],


    packageDirectory: Path,


    workingDirectory: Path,


    tool: PluginContext.Tool


    )
    ->
    [Command] {


    // . ..

    return [


    .prebuildCommand(


    displayName: "SwiftLint",


    executable: tool.path,


    arguments: arguments,


    outputFilesDirectory: outputFilesDirectory


    )


    ]


    }


    }


    https://github.com/realm/SwiftLint/blob/main/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift

    View full-size slide

  40. import Foundation


    import PackagePlugin


    @main


    struct SwiftLintPlugin: BuildToolPlugin {


    func createBuildCommands(context: PluginContext, target: Target) async throws
    -
    >
    [Command] {


    guard let sourceTarget = target as? SourceModuleTarget else {


    return []


    }


    return createBuildCommands(


    inputFiles: sourceTarget.sourceFiles(withSuffix: "swift").map(\.path),


    packageDirectory: context.package.directory,


    workingDirectory: context.pluginWorkDirectory,


    tool: try context.tool(named: "swiftlint")


    )


    }


    private func createBuildCommands(


    inputFiles: [Path],


    packageDirectory: Path,


    workingDirectory: Path,


    tool: PluginContext.Tool


    )
    ->
    [Command] {


    // . ..

    return [


    .prebuildCommand(


    displayName: "SwiftLint",


    executable: tool.path,


    arguments: arguments,


    outputFilesDirectory: outputFilesDirectory


    )


    ]


    }


    }


    https://github.com/realm/SwiftLint/blob/main/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift

    View full-size slide

  41. import Foundation


    import PackagePlugin


    @main


    struct SwiftLintPlugin: BuildToolPlugin {


    func createBuildCommands(context: PluginContext, target: Target) async throws
    -
    >
    [Command] {


    guard let sourceTarget = target as? SourceModuleTarget else {


    return []


    }


    return createBuildCommands(


    inputFiles: sourceTarget.sourceFiles(withSuffix: "swift").map(\.path),


    packageDirectory: context.package.directory,


    workingDirectory: context.pluginWorkDirectory,


    tool: try context.tool(named: "swiftlint")


    )


    }


    private func createBuildCommands(


    inputFiles: [Path],


    packageDirectory: Path,


    workingDirectory: Path,


    tool: PluginContext.Tool


    )
    ->
    [Command] {


    // . ..

    return [


    .prebuildCommand(


    displayName: "SwiftLint",


    executable: tool.path,


    arguments: arguments,


    outputFilesDirectory: outputFilesDirectory


    )


    ]


    }


    }


    https://github.com/realm/SwiftLint/blob/main/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift

    View full-size slide

  42. //
    swift-tools-version: 5.9


    import PackageDescription


    let package = Package(


    name: "DemoSwiftPackage",


    products: [


    .library(


    name: "DemoSwiftPackage",


    targets: ["DemoSwiftPackage"]


    ),


    ],


    dependencies: [


    .package(url: "https:
    //
    github.com/realm/SwiftLint.git", exact: "0.52.4")


    ],


    targets: [


    .target(


    name: "DemoSwiftPackage",


    plugins: [


    .plugin(name: "SwiftLintPlugin", package: "SwiftLint")


    ]


    )


    ]


    )


    View full-size slide

  43. //
    swift-tools-version: 5.9


    import PackageDescription


    let package = Package(


    name: "DemoSwiftPackage",


    products: [


    .library(


    name: "DemoSwiftPackage",


    targets: ["DemoSwiftPackage"]


    ),


    ],


    dependencies: [


    .package(url: "https:
    //
    github.com/realm/SwiftLint.git", exact: "0.52.4")


    ],


    targets: [


    .target(


    name: "DemoSwiftPackage",


    plugins: [


    .plugin(name: "SwiftLintPlugin", package: "SwiftLint")


    ]


    )


    ]


    )


    View full-size slide

  44. What about
    Xcode
    projects?

    View full-size slide

  45. import Foundation


    import PackagePlugin


    #if canImport(XcodeProjectPlugin)


    import XcodeProjectPlugin


    extension SwiftLintPlugin: XcodeBuildToolPlugin {


    func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws
    -
    >
    [Command] {


    let inputFilePaths = target.inputFiles


    .filter { $0.type
    ==
    .source
    &&
    $0.path.extension
    ==
    "swift" }


    .map(\.path)


    return createBuildCommands(


    inputFiles: inputFilePaths,


    packageDirectory: context.xcodeProject.directory,


    workingDirectory: context.pluginWorkDirectory,


    tool: try context.tool(named: "swiftlint")


    )


    }


    }


    #endif


    https://github.com/realm/SwiftLint/blob/main/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift

    View full-size slide

  46. Manually
    running a
    build script?

    View full-size slide

  47. import UIKit


    public protocol Camera {


    func start()


    func stop()


    func capture(_ completion: @escaping (UIImage?)
    ->
    Void)


    func rotate()


    }

    View full-size slide

  48. import UIKit


    //
    Protocol to be matched


    protocol AutoMockable {}


    public protocol Camera: AutoMockable {


    func start()


    func stop()


    func capture(_ completion: @escaping (UIImage?)
    ->
    Void)


    func rotate()


    }

    View full-size slide

  49. ./sourcery
    --
    config ".sourcery.yml"

    View full-size slide

  50. //
    Generated using Sourcery 1.8.2 — https:
    //
    github.com/krzysztofzablocki/Sourcery


    //
    DO NOT EDIT


    //
    swiftlint:disable line_length


    //
    swiftlint:disable variable_name


    import Foundation


    #if os(iOS)
    ||
    os(tvOS)
    ||
    os(watchOS)


    import UIKit


    #elseif os(OSX)


    import AppKit


    #endif


    class CameraMock: Camera {


    //
    MARK: - start


    var startCallsCount = 0


    var startCalled: Bool {


    return startCallsCount > 0


    }


    var startClosure: (()
    ->
    Void)?


    func start() {


    startCallsCount += 1


    startClosure?()


    }


    //

    View full-size slide

  51. / /
    swift-tools-version:5.6


    import PackageDescription


    import Foundation


    let package = Package(


    name: "Sourcery",


    platforms: [ .macOS(.v12) ],


    products: [


    .plugin(name: "SourceryCommandPlugin", targets: ["SourceryCommandPlugin"])


    ],


    dependencies: [],


    targets: [


    .executableTarget(


    name: "SourceryExecutable",


    dependencies: ["SourceryLib"],


    path: "SourceryExecutable",


    exclude: [


    "Info.plist"


    ]


    ),


    .plugin(


    name: "SourceryCommandPlugin",


    capability: .command(


    intent: .custom(


    verb: "sourcery-command",


    description: "Sourcery command plugin for code generation"


    ),


    permissions: [


    .writeToPackageDirectory(reason: "Need permission to write generated files to package directory")


    ]


    ),


    dependencies: ["SourceryExecutable"]


    )


    ]


    ) https://github.com/krzysztofzablocki/Sourcery/blob/master/Package.swift

    View full-size slide

  52. / /
    swift-tools-version:5.6


    import PackageDescription


    import Foundation


    let package = Package(


    name: "Sourcery",


    platforms: [ .macOS(.v12) ],


    products: [


    .plugin(name: "SourceryCommandPlugin", targets: ["SourceryCommandPlugin"])


    ],


    dependencies: [],


    targets: [


    .executableTarget(


    name: "SourceryExecutable",


    dependencies: ["SourceryLib"],


    path: "SourceryExecutable",


    exclude: [


    "Info.plist"


    ]


    ),


    .plugin(


    name: "SourceryCommandPlugin",


    capability: .command(


    intent: .custom(


    verb: "sourcery-command",


    description: "Sourcery command plugin for code generation"


    ),


    permissions: [


    .writeToPackageDirectory(reason: "Need permission to write generated files to package directory")


    ]


    ),


    dependencies: ["SourceryExecutable"]


    )


    ]


    ) https://github.com/krzysztofzablocki/Sourcery/blob/master/Package.swift

    View full-size slide

  53. / /
    swift-tools-version:5.6


    import PackageDescription


    import Foundation


    let package = Package(


    name: "Sourcery",


    platforms: [ .macOS(.v12) ],


    products: [


    .plugin(name: "SourceryCommandPlugin", targets: ["SourceryCommandPlugin"])


    ],


    dependencies: [],


    targets: [


    .executableTarget(


    name: "SourceryExecutable",


    dependencies: ["SourceryLib"],


    path: "SourceryExecutable",


    exclude: [


    "Info.plist"


    ]


    ),


    .plugin(


    name: "SourceryCommandPlugin",


    capability: .command(


    intent: .custom(


    verb: "sourcery-command",


    description: "Sourcery command plugin for code generation"


    ),


    permissions: [


    .writeToPackageDirectory(reason: "Need permission to write generated files to package directory")


    ]


    ),


    dependencies: ["SourceryExecutable"]


    )


    ]


    ) https://github.com/krzysztofzablocki/Sourcery/blob/master/Package.swift

    View full-size slide

  54. import PackagePlugin


    import Foundation


    @main


    struct SourceryCommandPlugin: CommandPlugin {


    func performCommand(context: PluginContext, arguments: [String]) async throws {


    / /
    Run one per target


    for target in context.package.targets {


    let configFilePath = target.directory.appending(subpath: ".sourcery.yml").string


    let sourcery = try context.tool(named: "SourceryExecutable").path.string




    guard FileManager.default.fileExists(atPath: configFilePath) else {


    Diagnostics.warning("⚠ Could not find `.sourcery.yml` for target \(target.name)")


    continue


    }


    let sourceryURL = URL(fileURLWithPath: sourcery)




    let process = Process()


    process.executableURL = sourceryURL


    process.arguments = [


    "
    - -
    config",


    configFilePath,


    "
    - -
    cacheBasePath",


    context.pluginWorkDirectory.string


    ]




    try process.run()


    process.waitUntilExit()




    let gracefulExit = process.terminationReason
    =
    =
    .exit
    &&
    process.terminationStatus
    ==
    0


    if !gracefulExit {


    throw "🛑 The plugin execution failed with reason: \(process.terminationReason.rawValue) and status: \(process.terminationStatus) "


    }


    }


    }


    }
    https://github.com/krzysztofzablocki/Sourcery/blob/master/Plugins/SourceryCommandPlugin/SourceryCommandPlugin.swift

    View full-size slide

  55. //
    swift-tools-version: 5.9


    import PackageDescription


    let package = Package(


    name: "GitHookExecutable",


    dependencies: [


    .package(url: "https:
    //
    github.com/apple/swift-argument-parser.git", exact: "1.2.3")


    ],


    targets: [


    .executableTarget(


    name: "GitHookExecutable",


    dependencies: [


    .product(name: "ArgumentParser", package: "swift-argument-parser")


    ]


    ),


    ]


    )


    View full-size slide

  56. //
    swift-tools-version: 5.9


    import PackageDescription


    let package = Package(


    name: "GitHookExecutable",


    dependencies: [


    .package(url: "https:
    //
    github.com/apple/swift-argument-parser.git", exact: "1.2.3"),


    .package(url: "https:
    //
    github.com/krzysztofzablocki/Sourcery.git", exact: "2.0.3")


    ],


    targets: [


    .executableTarget(


    name: "GitHookExecutable",


    dependencies: [


    .product(name: "ArgumentParser", package: "swift-argument-parser")


    ]


    ),


    ]


    )


    View full-size slide

  57. swift package sourcery-command

    View full-size slide

  58. What about
    Xcode
    projects?

    View full-size slide

  59. #if canImport(XcodeProjectPlugin)


    import XcodeProjectPlugin


    extension SourceryCommandPlugin: XcodeCommandPlugin {


    func performCommand(context: XcodePluginContext, arguments: [String]) throws {


    for target in context.xcodeProject.targets {


    guard let configFilePath = target


    .inputFiles


    .filter({ $0.path.lastComponent
    ==
    ".sourcery.yml" })


    .first?


    .path


    .string else {


    Diagnostics.warning("⚠ Could not find `.sourcery.yml` in Xcode's input file list")


    return


    }


    let sourcery = try context.tool(named: "SourceryExecutable").path.string




    try run(sourcery, withConfig: configFilePath, cacheBasePath: context.pluginWorkDirectory.string)


    }


    }


    }


    #endif


    https://github.com/krzysztofzablocki/Sourcery/blob/master/Plugins/SourceryCommandPlugin/SourceryCommandPlugin.swift

    View full-size slide

  60. #3 Back-End
    service?

    View full-size slide

  61. How does
    this work?

    View full-size slide

  62. Ok… but
    what are web
    hooks?

    View full-size slide

  63. issue_comment webhook
    https://docs.github.com/en/webhooks/webhook-events-and-payloads#issue_comment

    View full-size slide

  64. Creating a
    service with
    Swift!

    View full-size slide

  65. https://github.com/swift-server/swift-aws-lambda-runtime

    View full-size slide

  66. It’s just an
    executable…

    View full-size slide

  67. import AWSLambdaRuntime


    import AWSLambdaEvents


    @main


    struct UploadToTestFlightWebhook: SimpleLambdaHandler {


    let snakeCaseDecoder: JSONDecoder


    init() {


    let decoder = JSONDecoder()


    decoder.keyDecodingStrategy = .convertFromSnakeCase


    snakeCaseDecoder = decoder


    }


    func handle(_ request: APIGatewayV2Request, context: LambdaContext) async throws
    ->
    APIGatewayV2Response {


    guard let body = request.body,


    let bodyData = body.data(using: .utf8),


    let request = try? snakeCaseDecoder.decode(GithubWebhookData.self, from: bodyData) else {


    return .init(statusCode: .badRequest, body: "Could not parse the request content
    ...
    ")


    }


    guard request.action
    ==
    "created",


    request.comment.body.lowercased()
    ==
    "upload to testflight" else {


    return .init(statusCode: .ok, body: "Not handling the event
    ...
    ")


    }


    // ...

    }


    }

    View full-size slide

  68. … that runs
    on a different
    platform

    View full-size slide

  69. #!
    /bin/bash


    product=Webhook


    docker run \


    --
    rm \


    --
    volume "$(pwd)/:/src" \


    --
    workdir "/src/" \


    swift:5.8-amazonlinux2 \


    swift build
    --
    product $product -c release -Xswiftc -static-stdlib


    target=.build/lambda/$product


    rm -rf "$target"


    mkdir -p "$target"


    cp ".build/release/$product" "$target/"


    cd "$target"


    ln -s "$product" "bootstrap"


    zip
    --
    symlinks lambda.zip *
    https://fabianfett.dev/getting-started-with-swift-aws-lambda-runtime

    View full-size slide

  70. #!
    /bin/bash


    swift package \


    --
    disable-sandbox archive \


    --
    swift-version 5.8 \


    --
    output-path . \


    --
    products Webhook

    View full-size slide

  71. #!
    /bin/bash


    swift package \


    --
    disable-sandbox archive \


    --
    swift-version 5.8 \


    --
    output-path . \


    --
    products Webhook

    View full-size slide

  72. It’s getting better…

    View full-size slide

  73. You still need
    to upload it…

    View full-size slide

  74. service: upload-to-testflight-webhook


    frameworkVersion: "3"


    configValidationMode: warn


    package:


    artifact: build/Products/Webhook.zip


    provider:


    name: aws


    region: eu-west-1


    httpApi:


    payload: "2.0"


    cors: true


    runtime: provided.al2


    architecture: arm64


    functions:


    webhook:


    handler: webhook


    memorySize: 256


    description: "[${sls:stage}] Upload"


    events:


    - httpApi:


    path: /upload


    method: post
    Serverless
    framework
    https://www.serverless.com

    View full-size slide

  75. name: Deploy the serverless lambda to AWS


    on:


    push:


    branches:


    - main


    jobs:


    deploy:


    runs-on: ubuntu-latest


    container:


    image: swift:5.8-amazonlinux2


    steps:


    - uses: actions/checkout@v3


    - name: Configure AWS Credentials


    uses: aws-actions/configure-aws-credentials@v2


    with:


    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}


    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}


    aws-region: eu-west-2


    - name: Install zip


    run: |


    yum install -y zip


    - name: Install awscli


    run: |


    yum install -y awscli


    - name: Archive the lambda


    run: |


    ./package.sh Webhook


    - name: Deploy to AWS


    run: |


    aws lambda update-function-code \


    -
    -
    function-name webhook \


    -
    -
    zip-file fileb:
    //
    .build/lambda/Webhook/Webhook.zip


    CI/CD
    workflow
    https://www.polpiella.dev/automatic-deployment-of-swift-aws-lambdas-on-ci-cd

    View full-size slide

  76. This is jut the
    tip of the
    iceberg…

    View full-size slide

  77. Even slide decks!
    https://github.com/joshdholtz/DeckUI

    View full-size slide

  78. Any questions?

    View full-size slide

  79. Any questions?
    I have stickers too…

    View full-size slide

  80. Resources - Plugins
    https://www.polpiella.dev/code-generation-using-swift-package-plugins
    https://www.polpiella.dev/sourcery-swift-package-command-plugin
    https://www.polpiella.dev/load-custom-fonts-with-no-code-using-swift-package-plugins
    https://www.polpiella.dev/an-early-look-at-swift-extensible-build-tools
    https://theswiftdev.com/beginners-guide-to-swift-package-manager-command-plugins/
    https://www.polpiella.dev/binary-targets-in-modern-swift-packages
    https://www.polpiella.dev/network-requests-in-swift-package-plugins

    View full-size slide

  81. https://www.polpiella.dev/automating-swift-package-releases-with-github-actions
    https://www.polpiella.dev/collecting-gihub-actions-workflow-metrics-with-swift
    https://www.polpiella.dev/scripting-in-swift-git-hooks
    https://github.com/artemnovichkov/Swift-For-Scripting
    https://blog.eidinger.info/swift-e-runs-code-directly-from-the-command-line
    https://nshipster.com/swift-sh/
    Resources - CLI + Scripts

    View full-size slide

  82. https://www.areweserveryet.org/
    https://www.polpiella.dev/github-webhooks-and-xcode-cloud
    https://www.polpiella.dev/swift-async-await-in-aws-lambdas
    https://www.polpiella.dev/scheduled-swift-aws-lambdas
    https://www.polpiella.dev/automatic-deployment-of-swift-aws-lambdas-on-ci-cd
    https://opticalaberration.com/2021/12/proxy-server.html
    Resources - ServerSide Swift
    https://www.serversideswift.info
    https://theswiftdev.com

    View full-size slide