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 Slide

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

    View Slide

  3. Content creation
    Apps I am working on
    Public speaking

    View Slide

  4. Content creation
    Apps I am working on
    Public speaking

    View Slide

  5. Content creation
    Apps I am working on
    Public speaking

    View Slide

  6. Swift &
    Developer
    Tools?

    View Slide

  7. Swift &
    Developer
    Tools!

    View Slide

  8. You know
    Swift!

    View Slide

  9. You (and your
    team) know
    Swift!

    View Slide

  10. Focus on the
    system and NOT
    the language!

    View Slide

  11. You (and your
    audience)
    know Swift!

    View Slide

  12. Can you build
    it with Swift?

    View Slide

  13. #1 A simple
    CLI script?

    View Slide

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

    View 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 Slide

  16. View Slide

  17. View Slide

  18. # !
    /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 Slide

  19. # !
    /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 Slide

  20. chmod +x file_where_hook_code_is

    View Slide

  21. Not so fast…

    View Slide

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

    View Slide

  23. # !
    /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 Slide

  24. # !
    /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 Slide

  25. Future-
    proofing and
    scaling

    View Slide

  26. swift package init
    --
    type executable

    View Slide

  27. //
    swift-tools-version: 5.9


    import PackageDescription


    let package = Package(


    name: "GitHookExecutable",


    dependencies: [


    ],


    targets: [


    .executableTarget(


    name: "GitHookExecutable",


    dependencies: [


    ]


    ),


    ]


    )

    View Slide

  28. //
    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 Slide

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

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

    View Slide

  31. #2 Running a
    build script?

    View Slide

  32. View Slide

  33. brew install swiftlint
    pod install

    View Slide

  34. What about
    Swift
    Packages?

    View Slide

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

    View 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 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 Slide

  38. //
    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 Slide

  39. //
    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 Slide

  40. //
    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 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 Slide

  42. 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 Slide

  43. 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 Slide

  44. 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 Slide

  45. //
    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 Slide

  46. //
    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 Slide

  47. What about
    Xcode
    projects?

    View Slide

  48. 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 Slide

  49. View Slide

  50. View Slide

  51. Manually
    running a
    build script?

    View Slide

  52. import UIKit


    public protocol Camera {


    func start()


    func stop()


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


    func rotate()


    }

    View Slide

  53. 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 Slide

  54. ./sourcery
    --
    config ".sourcery.yml"

    View Slide

  55. //
    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 Slide

  56. What if…

    View Slide

  57. / /
    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 Slide

  58. / /
    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 Slide

  59. / /
    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 Slide

  60. 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 Slide

  61. //
    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 Slide

  62. //
    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 Slide

  63. swift package sourcery-command

    View Slide

  64. View Slide

  65. What about
    Xcode
    projects?

    View Slide

  66. #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 Slide

  67. View Slide

  68. View Slide

  69. #3 Back-End
    service?

    View Slide

  70. View Slide

  71. View Slide

  72. How does
    this work?

    View Slide

  73. Ok… but
    what are web
    hooks?

    View Slide

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

    View Slide

  75. Creating a
    service with
    Swift!

    View Slide

  76. View Slide

  77. View Slide

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

    View Slide

  79. It’s just an
    executable…

    View Slide

  80. 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 Slide

  81. … that runs
    on a different
    platform

    View Slide

  82. #!
    /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 Slide

  83. #!
    /bin/bash


    swift package \


    --
    disable-sandbox archive \


    --
    swift-version 5.8 \


    --
    output-path . \


    --
    products Webhook

    View Slide

  84. #!
    /bin/bash


    swift package \


    --
    disable-sandbox archive \


    --
    swift-version 5.8 \


    --
    output-path . \


    --
    products Webhook

    View Slide

  85. It’s getting better…

    View Slide

  86. You still need
    to upload it…

    View Slide

  87. View Slide

  88. 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 Slide

  89. 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 Slide

  90. This is jut the
    tip of the
    iceberg…

    View Slide

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

    View Slide

  92. Any questions?

    View Slide

  93. Any questions?
    I have stickers too…

    View Slide

  94. 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 Slide

  95. 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 Slide

  96. 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 Slide