Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
[iOS Dev UK 23] Making developer tools with Swift
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Pol Piella Abadia
September 05, 2023
Programming
2.4k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
[iOS Dev UK 23] Making developer tools with Swift
Pol Piella Abadia
September 05, 2023
More Decks by Pol Piella Abadia
See All by Pol Piella Abadia
[Do iOS '24] Ship your app on a Friday...and enjoy your weekend!
polpielladev
0
1.4k
[SwiftConf '24] Shipping your apps should be fast and easy
polpielladev
0
2.2k
[Workshop] Ship your apps faster with Xcode Cloud
polpielladev
0
150
[SwiftCraft '24] Back to the Future: Swift 6 Edition!
polpielladev
0
2.3k
[London Tech Leaders x AppCircle] The future of mobile releases
polpielladev
0
2.2k
[Swift Heroes '24] Delightful on-device AI experiences
polpielladev
0
2.3k
[SwiftLeeds '23] Delightful Swift CLI applications
polpielladev
0
2.1k
[NSBarcelona/AppTalks Manchester] - Delightful Swift CLI applications
polpielladev
0
2.1k
[Swift Heroes 2023] Making developer tools with Swift
polpielladev
0
2.3k
Other Decks in Programming
See All in Programming
PHPで使える日時の表現と、その知り方 #frontend_phpcon_do
o0h
PRO
0
230
キャリア迷子上等 ─ "ない道"は自分で作ればいい
16bitidol
3
2k
Lemonade + Foundry Toolkit でお手軽アプリ開発
seosoft
1
320
スマートグラスで並列バイブコーディング
hyshu
0
120
TypeScript+Orvalで実現する型安全かつ堅牢でスケーラブルなマルチチャネル通知基盤 / TSKaigi Night talks ~after conference~
d0riven
0
330
LLM本来の能力を解き放つサンドボックス技術とAI民主化への適用
yukukotani
3
3.6k
Observability in Practice:Grafana 與 Edge Device SRE 的那些事
blueswen
0
160
ユニットテストの先へ:テスト技法で要求・仕様を整理するJava開発実践 / Beyond_Unit_Testing_Practical_Java_Development_Techniques_for_Organizing_Requirements_and_Specifications
shimashima35
0
390
The NotImplementedError Problem in Ruby
koic
1
710
コンテキストの使い捨てをやめる — ビジネスルール駆動開発と miko —
ioki
0
190
jQueryをバージョンアップする前に使いたいjQuery Migrate
matsuo_atsushi
0
200
LLM Plugin for Node-REDの利用方法と開発について
404background
0
170
Featured
See All Featured
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
1
250
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
201
75k
Documentation Writing (for coders)
carmenintech
77
5.4k
Exploring the Power of Turbo Streams & Action Cable | RailsConf2023
kevinliebholz
37
6.5k
We Have a Design System, Now What?
morganepeng
55
8.2k
How GitHub (no longer) Works
holman
316
150k
Art, The Web, and Tiny UX
lynnandtonic
304
22k
Visualization
eitanlees
152
17k
Lessons Learnt from Crawling 1000+ Websites
charlesmeaden
PRO
1
1.3k
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
300
DevOps and Value Stream Thinking: Enabling flow, efficiency and business value
helenjbeal
1
230
Imperfection Machines: The Place of Print at Facebook
scottboms
270
14k
Transcript
Making developer tools with Swift Pol Piella Abadia Senior iOS
Developer, BBC iPlayer iOS Dev UK · Aberystwyth, Wales 10:20 Great Hall
Senior software engineer based in Manchester Hi! I’m Pol
Content creation Apps I am working on Public speaking
Content creation Apps I am working on Public speaking
Content creation Apps I am working on Public speaking
Swift & Developer Tools?
Swift & Developer Tools!
You know Swift!
You (and your team) know Swift!
Focus on the system and NOT the language!
You (and your audience) know Swift!
Can you build it with Swift?
#1 A simple CLI script?
feature/MYTICKET-123_name-of-your-branch
feature/MYTICKET-123_name-of-your-branch Author: Pol Piella Abadia <
[email protected]
> Date: Sun Aug 20
14:03:41 2023 +0100 My awesome feature TICKET-123 Branch Commit
None
None
# ! /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)") } }
# ! /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)") } }
chmod +x file_where_hook_code_is
Not so fast…
Yes and no… https://github.com/mxcl/swift-sh
# ! /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)") } }
# ! /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)") } }
Future- proofing and scaling
swift package init -- type executable
// swift-tools-version: 5.9 import PackageDescription let package = Package( name:
"GitHookExecutable", dependencies: [ ], targets: [ .executableTarget( name: "GitHookExecutable", dependencies: [ ] ), ] )
// 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") ] ), ] )
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) // . .. } }
Learn more about it! https://www.youtube.com/watch?v=yyOXynCoagE&t=611s
#2 Running a build script?
None
brew install swiftlint pod install
What about Swift Packages?
Swift 5.6 to the rescue! https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md
// 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
// 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
// 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
// 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
// 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
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
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
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
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
// 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") ] ) ] )
// 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") ] ) ] )
What about Xcode projects?
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
None
None
Manually running a build script?
import UIKit public protocol Camera { func start() func stop()
func capture(_ completion: @escaping (UIImage?) -> Void) func rotate() }
import UIKit // Protocol to be matched protocol AutoMockable {}
public protocol Camera: AutoMockable { func start() func stop() func capture(_ completion: @escaping (UIImage?) -> Void) func rotate() }
./sourcery -- config ".sourcery.yml"
// 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?() } //
What if…
/ / 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
/ / 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
/ / 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
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
// 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") ] ), ] )
// 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") ] ), ] )
swift package sourcery-command
None
What about Xcode projects?
#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
None
None
#3 Back-End service?
None
None
How does this work?
Ok… but what are web hooks?
issue_comment webhook https://docs.github.com/en/webhooks/webhook-events-and-payloads#issue_comment
Creating a service with Swift!
None
None
https://github.com/swift-server/swift-aws-lambda-runtime
It’s just an executable…
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 ... ") } // ... } }
… that runs on a different platform
#! /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
#! /bin/bash swift package \ -- disable-sandbox archive \ --
swift-version 5.8 \ -- output-path . \ -- products Webhook
#! /bin/bash swift package \ -- disable-sandbox archive \ --
swift-version 5.8 \ -- output-path . \ -- products Webhook
It’s getting better…
You still need to upload it…
None
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
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
This is jut the tip of the iceberg…
Even slide decks! https://github.com/joshdholtz/DeckUI
Any questions?
Any questions? I have stickers too…
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
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
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