-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
065ecfe
commit 75016a6
Showing
4 changed files
with
183 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,171 @@ | ||
import ArgumentParser | ||
import Foundation | ||
|
||
print("Here it is") | ||
enum DestinationSpecifier { | ||
case platform(String) | ||
case deviceID(String) | ||
|
||
var xcodebuildArgument: String { | ||
switch self { | ||
case let .platform(platform): | ||
"platform=\(platform)" | ||
case let .deviceID(deviceID): | ||
"id=\(deviceID)" | ||
} | ||
} | ||
} | ||
|
||
enum DestinationStrategy { | ||
case fixed(platform: String) | ||
case lookup(destinationPredicate: DestinationPredicate) | ||
} | ||
|
||
struct DestinationPredicate { | ||
// TODO: document | ||
var runtime: String | ||
var deviceType: String | ||
} | ||
|
||
enum Platform: String, CaseIterable { | ||
case macOS | ||
case iOS | ||
case tvOS | ||
|
||
var destinationStrategy: DestinationStrategy { | ||
// TODO: why is xcodebuild giving locally with iOS "--- xcodebuild: WARNING: Using the first of multiple matching destinations:" | ||
switch self { | ||
case .macOS: | ||
.fixed(platform: "macOS") | ||
case .iOS: | ||
.lookup(destinationPredicate: .init(runtime: "iOS-17-5", deviceType: "iPhone-15")) | ||
case .tvOS: | ||
.lookup(destinationPredicate: .init(runtime: "tvOS-17-5", deviceType: "Apple TV")) | ||
} | ||
} | ||
} | ||
|
||
extension Platform: ExpressibleByArgument { | ||
init?(argument: String) { | ||
self.init(rawValue: argument) | ||
} | ||
} | ||
|
||
struct SimctlOutput: Codable { | ||
var devices: [String: [Device]] | ||
|
||
struct Device: Codable { | ||
var udid: String | ||
var deviceTypeIdentifier: String | ||
} | ||
} | ||
|
||
enum Error: Swift.Error { | ||
case terminatedWithExitCode(Int32) | ||
} | ||
|
||
// 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? | ||
@available(macOS 14, *) | ||
@main | ||
struct BuildTool: ParsableCommand { | ||
@Option var platform: Platform | ||
|
||
@Option var swiftVersion: Int | ||
|
||
mutating func run() throws { | ||
let destinationSpecifier: DestinationSpecifier = switch platform.destinationStrategy { | ||
case let .fixed(platform): | ||
.platform(platform) | ||
case let .lookup(destinationPredicate): | ||
try .deviceID(fetchDeviceUDID(destinationPredicate: destinationPredicate)) | ||
} | ||
|
||
try runXcodebuild(action: nil, destination: destinationSpecifier) | ||
try runXcodebuild(action: "test", destination: destinationSpecifier) | ||
} | ||
|
||
func runXcodebuild(action: String?, destination: DestinationSpecifier) throws { | ||
var arguments: [String] = [] | ||
|
||
if let action { | ||
arguments.append(action) | ||
} | ||
|
||
arguments.append(contentsOf: ["-scheme", "AblyChat"]) | ||
arguments.append(contentsOf: ["-destination", destination.xcodebuildArgument]) | ||
|
||
arguments.append(contentsOf: [ | ||
"SWIFT_TREAT_WARNINGS_AS_ERRORS=YES", | ||
"SWIFT_VERSION=\(swiftVersion)", | ||
]) | ||
|
||
try run(executableName: "xcodebuild", arguments: arguments) | ||
} | ||
|
||
func fetchDeviceUDID(destinationPredicate: DestinationPredicate) throws -> String { | ||
let simctlOutput = try fetchSimctlOutput() | ||
|
||
let runtimeIdentifier = "com.apple.CoreSimulator.SimRuntime.\(destinationPredicate.runtime)" | ||
let deviceTypeIdentifier = "com.apple.CoreSimulator.SimDeviceType.\(destinationPredicate.deviceType)" | ||
guard let matchingDevices = simctlOutput.devices[runtimeIdentifier]?.filter({ $0.deviceTypeIdentifier == deviceTypeIdentifier }) else { | ||
fatalError("Couldn’t find a simulator with runtime \(runtimeIdentifier) and device type \(deviceTypeIdentifier); available devices are \(simctlOutput.devices)") | ||
} | ||
|
||
if matchingDevices.count > 1 { | ||
fatalError("Found multiple simulators with runtime \(runtimeIdentifier) and device type \(deviceTypeIdentifier); matching devices are \(matchingDevices)") | ||
} | ||
|
||
return matchingDevices[0].udid | ||
} | ||
|
||
func fetchSimctlOutput() throws -> SimctlOutput { | ||
let data = try runAndReturnStdout( | ||
executableName: "xcrun", | ||
arguments: ["simctl", "list", "--json", "devices", "available"] | ||
) | ||
|
||
return try JSONDecoder().decode(SimctlOutput.self, from: data) | ||
} | ||
|
||
// 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. | ||
private func run(executableName: String, arguments: [String]) throws { | ||
let process = Process() | ||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env") | ||
process.arguments = [executableName] + arguments | ||
|
||
try process.run() | ||
process.waitUntilExit() | ||
|
||
if process.terminationStatus != 0 { | ||
throw Error.terminatedWithExitCode(process.terminationStatus) | ||
} | ||
} | ||
|
||
// 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. | ||
private func runAndReturnStdout(executableName: String, arguments: [String]) throws -> Data { | ||
let process = Process() | ||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env") | ||
process.arguments = [executableName] + arguments | ||
|
||
let standardOutput = Pipe() | ||
process.standardOutput = standardOutput | ||
|
||
try process.run() | ||
|
||
var stdoutData = Data() | ||
while true { | ||
if let data = try standardOutput.fileHandleForReading.readToEnd() { | ||
stdoutData.append(data) | ||
} else { | ||
break | ||
} | ||
} | ||
|
||
process.waitUntilExit() | ||
|
||
if process.terminationStatus != 0 { | ||
throw Error.terminatedWithExitCode(process.terminationStatus) | ||
} | ||
|
||
return stdoutData | ||
} | ||
} |