diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 5f7d29b..499c88b 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -59,6 +59,5 @@ jobs: with: xcode-version: ${{ matrix.tooling.xcode-version }} - # TODO OS version, and ambiguity in platforms, and no match - - run: xcodebuild -scheme AblyChat -destination "platform=${{ matrix.platform }}" SWIFT_TREAT_WARNINGS_AS_ERRORS=YES SWIFT_VERSION=${{ matrix.tooling.swift-version }} - - run: xcodebuild test -scheme AblyChat -destination "platform=${{ matrix.platform }}" SWIFT_TREAT_WARNINGS_AS_ERRORS=YES SWIFT_VERSION=${{ matrix.tooling.swift-version }} + - run: swift run BuildTool --platform ${{ matrix.platform }} --swift-version ${{ matrix.swift-version }} + working-directory: ./script diff --git a/Package.resolved b/Package.resolved index e3dae42..e65ca60 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "bb9e762d580eaf6bdfc4c2b4ef738357fe7528fca621f88bf37eab5f324406ae", + "originHash" : "10b6989ad831255bec51a3b6a49da12c471d57a9611e6834de10ee5a8863e652", "pins" : [ { "identity" : "ably-cocoa", @@ -28,15 +28,6 @@ "version" : "0.4.0" } }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", - "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" - } - }, { "identity" : "swiftformat", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index d02d79f..7c7c21c 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,6 @@ let package = Package( .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0"), .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.55.1"), .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.54.0"), - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -33,6 +32,5 @@ let package = Package( name: "AblyChatTests", dependencies: ["AblyChat"] ), - .executableTarget(name: "TestScript", dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")], swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]), ] ) diff --git a/Sources/TestScript/GetDestination.swift b/Sources/TestScript/GetDestination.swift deleted file mode 100644 index 0e35899..0000000 --- a/Sources/TestScript/GetDestination.swift +++ /dev/null @@ -1,83 +0,0 @@ -import ArgumentParser -import Foundation - -struct SimctlOutput: Codable { - var devices: [String: [Device]] - - struct Device: Codable { - var udid: String - var deviceTypeIdentifier: String - } -} - -@main -struct GetDestination: ParsableCommand { - static var configuration = CommandConfiguration(abstract: "Finds a simulator device matching given parameters, and writes its UUID to standard output.") - - @Option(help: "The part of the runtime identifier that comes after \"com.apple.CoreSimulator.SimRuntime.\". For example, \"iOS-17-4\".") - var runtime: String - - @Option(help: "The part of the device type identifier that comes after \"com.apple.CoreSimulator.SimDeviceType\". For example, \"iPhone-15\".") - var deviceType: String - - mutating func run() throws { - let simctlOutput = try fetchSimctlOutput() - - let runtimeIdentifier = "com.apple.CoreSimulator.SimRuntime.\(runtime)" - let deviceTypeIdentifier = "com.apple.CoreSimulator.SimDeviceType.\(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)") - } - - print(matchingDevices[0].udid) - } - - func fetchSimctlOutput() throws -> SimctlOutput { - let data = try runAndReturnStdout( - processPath: "/usr/bin/env", - arguments: ["xcrun", "simctl", "list", "--json"] - ) - - 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 runAndReturnStdout(processPath: String, arguments: [String]) throws -> Data { - let process = Process() - process.executableURL = URL(fileURLWithPath: processPath) - process.arguments = arguments - - let standardOutput = Pipe() - process.standardOutput = standardOutput - - let semaphore = DispatchSemaphore(value: 0) - - process.terminationHandler = { _ in - semaphore.signal() - } - - try process.run() - - var stdoutData = Data() - while true { - if #available(macOS 14, *) { - if let data = try standardOutput.fileHandleForReading.readToEnd() { - stdoutData.append(data) - } else { - break - } - } else { - // TODO: this is annoying, we are stuck with the deployment target of the library - fatalError("Expected to be running on macOS 14") - } - } - - semaphore.wait() - - return stdoutData - } -} diff --git a/script/Package.resolved b/script/Package.resolved new file mode 100644 index 0000000..62310a4 --- /dev/null +++ b/script/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "422fe248db2d0a796fdbfcd73b31069c259a3cc7bfb84dd769af3bd9c6fd84c4", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + } + ], + "version" : 3 +} diff --git a/script/Package.swift b/script/Package.swift new file mode 100644 index 0000000..a567a50 --- /dev/null +++ b/script/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +// TODO: explain why a separate package + +let package = Package( + name: "AblyChatBuildTools", + platforms: [ + .macOS(.v14), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + ], + targets: [ + .executableTarget(name: "BuildTool", dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")], swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]), + ] +) diff --git a/script/Sources/BuildTool/BuildTool.swift b/script/Sources/BuildTool/BuildTool.swift new file mode 100644 index 0000000..f07b994 --- /dev/null +++ b/script/Sources/BuildTool/BuildTool.swift @@ -0,0 +1,167 @@ +import ArgumentParser +import Foundation + +struct DestinationPredicate { + // TODO: document + var runtime: String + var deviceType: String +} + +enum Platform: String { + case macOS + case iOS + case tvOS + + // TODO: why is xcodebuild giving locally with iOS "--- xcodebuild: WARNING: Using the first of multiple matching destinations:" + var destinationPredicate: DestinationPredicate? { + switch self { + case .macOS: nil + case .iOS: .init(runtime: "iOS-17-4", deviceType: "iPhone-15") + case .tvOS: .init(runtime: "tvOS", deviceType: "TODO") + } + } +} + +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) +} + +@main +struct BuildTool: ParsableCommand { + @Option + var platform: Platform + + @Option + var swiftVersion: Int + + mutating func run() throws { + print("got platform \(platform)") + let deviceUDID: String? = if let destinationPredicate = platform.destinationPredicate { + try fetchDeviceUDID(destinationPredicate: destinationPredicate) + } else { + nil + } + + // This tool runs from the repo’s `script` directory, so change into the repo root + FileManager.default.changeCurrentDirectoryPath("..") + try runXcodebuild(action: nil, deviceUDID: deviceUDID) + try runXcodebuild(action: "test", deviceUDID: deviceUDID) + } + + func runXcodebuild(action: String?, deviceUDID: String?) throws { + var arguments: [String] = [] + + if let action { + arguments.append(action) + } + + arguments.append(contentsOf: ["-scheme", "AblyChat"]) + + if let deviceUDID { + arguments.append(contentsOf: ["-destination", "id=\(deviceUDID)"]) + } + + 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"] + ) + + 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 + + let semaphore = DispatchSemaphore(value: 0) + + process.terminationHandler = { _ in + semaphore.signal() + } + + try process.run() + + semaphore.wait() + + 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 + + let semaphore = DispatchSemaphore(value: 0) + + process.terminationHandler = { _ in + semaphore.signal() + } + + try process.run() + + var stdoutData = Data() + while true { + if let data = try standardOutput.fileHandleForReading.readToEnd() { + stdoutData.append(data) + } else { + break + } + } + + semaphore.wait() + + if process.terminationStatus != 0 { + throw Error.terminatedWithExitCode(process.terminationStatus) + } + + return stdoutData + } +}