Skip to content

Commit 75016a6

Browse files
return
1 parent 065ecfe commit 75016a6

File tree

4 files changed

+183
-15
lines changed

4 files changed

+183
-15
lines changed

.github/workflows/check.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@ jobs:
6060
xcode-version: ${{ matrix.tooling.xcode-version }}
6161

6262
- name: Build and run tests
63-
run: swift run --verbose BuildTool --platform ${{ matrix.platform }} --swift-version ${{ matrix.tooling.swift-version }}
63+
run: swift run BuildTool --platform ${{ matrix.platform }} --swift-version ${{ matrix.tooling.swift-version }}

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ let package = Package(
1212
],
1313
products: [
1414
// Products define the executables and libraries a package produces, making them visible to other packages.
15-
/*.library(*/
16-
/*name: "AblyChat",*/
17-
/*targets: ["AblyChat"]*/
18-
/*),*/
15+
.library(
16+
name: "AblyChat",
17+
targets: ["AblyChat"]
18+
),
1919
],
2020
dependencies: [
2121
.package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0"),
@@ -26,13 +26,13 @@ let package = Package(
2626
targets: [
2727
// Targets are the basic building blocks of a package, defining a module or a test suite.
2828
// Targets can depend on other targets in this package and products from dependencies.
29-
/*.target(*/
30-
/*name: "AblyChat", dependencies: [.product(name: "Ably", package: "ably-cocoa")], swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]*/
31-
/*),*/
32-
/*.testTarget(*/
33-
/*name: "AblyChatTests",*/
34-
/*dependencies: ["AblyChat"]*/
35-
/*),*/
36-
.executableTarget(name: "BuildTool", dependencies: [], swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]),
29+
.target(
30+
name: "AblyChat", dependencies: [.product(name: "Ably", package: "ably-cocoa")], swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
31+
),
32+
.testTarget(
33+
name: "AblyChatTests",
34+
dependencies: ["AblyChat"]
35+
),
36+
.executableTarget(name: "BuildTool", dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")], swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]),
3737
]
3838
)

Sources/BuildTool/BuildTool.swift

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,171 @@
1+
import ArgumentParser
12
import Foundation
23

3-
print("Here it is")
4+
enum DestinationSpecifier {
5+
case platform(String)
6+
case deviceID(String)
7+
8+
var xcodebuildArgument: String {
9+
switch self {
10+
case let .platform(platform):
11+
"platform=\(platform)"
12+
case let .deviceID(deviceID):
13+
"id=\(deviceID)"
14+
}
15+
}
16+
}
17+
18+
enum DestinationStrategy {
19+
case fixed(platform: String)
20+
case lookup(destinationPredicate: DestinationPredicate)
21+
}
22+
23+
struct DestinationPredicate {
24+
// TODO: document
25+
var runtime: String
26+
var deviceType: String
27+
}
28+
29+
enum Platform: String, CaseIterable {
30+
case macOS
31+
case iOS
32+
case tvOS
33+
34+
var destinationStrategy: DestinationStrategy {
35+
// TODO: why is xcodebuild giving locally with iOS "--- xcodebuild: WARNING: Using the first of multiple matching destinations:"
36+
switch self {
37+
case .macOS:
38+
.fixed(platform: "macOS")
39+
case .iOS:
40+
.lookup(destinationPredicate: .init(runtime: "iOS-17-5", deviceType: "iPhone-15"))
41+
case .tvOS:
42+
.lookup(destinationPredicate: .init(runtime: "tvOS-17-5", deviceType: "Apple TV"))
43+
}
44+
}
45+
}
46+
47+
extension Platform: ExpressibleByArgument {
48+
init?(argument: String) {
49+
self.init(rawValue: argument)
50+
}
51+
}
52+
53+
struct SimctlOutput: Codable {
54+
var devices: [String: [Device]]
55+
56+
struct Device: Codable {
57+
var udid: String
58+
var deviceTypeIdentifier: String
59+
}
60+
}
61+
62+
enum Error: Swift.Error {
63+
case terminatedWithExitCode(Int32)
64+
}
65+
66+
// TODO: Is there a better way to make sure that this script has access to macOS APIs that are more recent than the package’s deployment target?
67+
@available(macOS 14, *)
68+
@main
69+
struct BuildTool: ParsableCommand {
70+
@Option var platform: Platform
71+
72+
@Option var swiftVersion: Int
73+
74+
mutating func run() throws {
75+
let destinationSpecifier: DestinationSpecifier = switch platform.destinationStrategy {
76+
case let .fixed(platform):
77+
.platform(platform)
78+
case let .lookup(destinationPredicate):
79+
try .deviceID(fetchDeviceUDID(destinationPredicate: destinationPredicate))
80+
}
81+
82+
try runXcodebuild(action: nil, destination: destinationSpecifier)
83+
try runXcodebuild(action: "test", destination: destinationSpecifier)
84+
}
85+
86+
func runXcodebuild(action: String?, destination: DestinationSpecifier) throws {
87+
var arguments: [String] = []
88+
89+
if let action {
90+
arguments.append(action)
91+
}
92+
93+
arguments.append(contentsOf: ["-scheme", "AblyChat"])
94+
arguments.append(contentsOf: ["-destination", destination.xcodebuildArgument])
95+
96+
arguments.append(contentsOf: [
97+
"SWIFT_TREAT_WARNINGS_AS_ERRORS=YES",
98+
"SWIFT_VERSION=\(swiftVersion)",
99+
])
100+
101+
try run(executableName: "xcodebuild", arguments: arguments)
102+
}
103+
104+
func fetchDeviceUDID(destinationPredicate: DestinationPredicate) throws -> String {
105+
let simctlOutput = try fetchSimctlOutput()
106+
107+
let runtimeIdentifier = "com.apple.CoreSimulator.SimRuntime.\(destinationPredicate.runtime)"
108+
let deviceTypeIdentifier = "com.apple.CoreSimulator.SimDeviceType.\(destinationPredicate.deviceType)"
109+
guard let matchingDevices = simctlOutput.devices[runtimeIdentifier]?.filter({ $0.deviceTypeIdentifier == deviceTypeIdentifier }) else {
110+
fatalError("Couldn’t find a simulator with runtime \(runtimeIdentifier) and device type \(deviceTypeIdentifier); available devices are \(simctlOutput.devices)")
111+
}
112+
113+
if matchingDevices.count > 1 {
114+
fatalError("Found multiple simulators with runtime \(runtimeIdentifier) and device type \(deviceTypeIdentifier); matching devices are \(matchingDevices)")
115+
}
116+
117+
return matchingDevices[0].udid
118+
}
119+
120+
func fetchSimctlOutput() throws -> SimctlOutput {
121+
let data = try runAndReturnStdout(
122+
executableName: "xcrun",
123+
arguments: ["simctl", "list", "--json", "devices", "available"]
124+
)
125+
126+
return try JSONDecoder().decode(SimctlOutput.self, from: data)
127+
}
128+
129+
// I would have liked to use Swift concurrency for this but it felt like it would be a bit of a faff and it’s only a script. There’s a proposal for a Subprocess API coming up in Foundation which will marry Process with Swift concurrency.
130+
private func run(executableName: String, arguments: [String]) throws {
131+
let process = Process()
132+
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
133+
process.arguments = [executableName] + arguments
134+
135+
try process.run()
136+
process.waitUntilExit()
137+
138+
if process.terminationStatus != 0 {
139+
throw Error.terminatedWithExitCode(process.terminationStatus)
140+
}
141+
}
142+
143+
// I would have liked to use Swift concurrency for this but it felt like it would be a bit of a faff and it’s only a script. There’s a proposal for a Subprocess API coming up in Foundation which will marry Process with Swift concurrency.
144+
private func runAndReturnStdout(executableName: String, arguments: [String]) throws -> Data {
145+
let process = Process()
146+
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
147+
process.arguments = [executableName] + arguments
148+
149+
let standardOutput = Pipe()
150+
process.standardOutput = standardOutput
151+
152+
try process.run()
153+
154+
var stdoutData = Data()
155+
while true {
156+
if let data = try standardOutput.fileHandleForReading.readToEnd() {
157+
stdoutData.append(data)
158+
} else {
159+
break
160+
}
161+
}
162+
163+
process.waitUntilExit()
164+
165+
if process.terminationStatus != 0 {
166+
throw Error.terminatedWithExitCode(process.terminationStatus)
167+
}
168+
169+
return stdoutData
170+
}
171+
}

0 commit comments

Comments
 (0)