This commit is contained in:
Matt Kiazyk 2023-11-23 10:37:41 -06:00
parent 487cbb0045
commit 6ffce23616
21 changed files with 237 additions and 231 deletions

View file

@ -79,7 +79,6 @@
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */; }; CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */; };
CAC28188259EE27200B8AB0B /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = CAC28187259EE27200B8AB0B /* CombineExpectations */; }; CAC28188259EE27200B8AB0B /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = CAC28187259EE27200B8AB0B /* CombineExpectations */; };
CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */; }; CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */; };
CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281D9259F985100B8AB0B /* InstallationStep.swift */; };
CAC281E2259FA44600B8AB0B /* Bundle+XcodesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */; }; CAC281E2259FA44600B8AB0B /* Bundle+XcodesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */; };
CAC281E7259FA45A00B8AB0B /* Environment+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281E6259FA45A00B8AB0B /* Environment+Mock.swift */; }; CAC281E7259FA45A00B8AB0B /* Environment+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281E6259FA45A00B8AB0B /* Environment+Mock.swift */; };
CAC9F92D25BCDA4400B4965F /* HelperInstallState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */; }; CAC9F92D25BCDA4400B4965F /* HelperInstallState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */; };
@ -269,7 +268,6 @@
CABFAA422593104F00380FEE /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; CABFAA422593104F00380FEE /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+InfoPlistValues.swift"; sourceTree = "<group>"; }; CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+InfoPlistValues.swift"; sourceTree = "<group>"; };
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingProgressIndicator.swift; sourceTree = "<group>"; }; CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingProgressIndicator.swift; sourceTree = "<group>"; };
CAC281D9259F985100B8AB0B /* InstallationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStep.swift; sourceTree = "<group>"; };
CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+XcodesTests.swift"; sourceTree = "<group>"; }; CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+XcodesTests.swift"; sourceTree = "<group>"; };
CAC281E6259FA45A00B8AB0B /* Environment+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Mock.swift"; sourceTree = "<group>"; }; CAC281E6259FA45A00B8AB0B /* Environment+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Mock.swift"; sourceTree = "<group>"; };
CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperInstallState.swift; sourceTree = "<group>"; }; CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperInstallState.swift; sourceTree = "<group>"; };
@ -482,7 +480,6 @@
CABFA9AC2592EEE900380FEE /* Foundation.swift */, CABFA9AC2592EEE900380FEE /* Foundation.swift */,
CA9FF9352595B44700E47BAF /* HelperClient.swift */, CA9FF9352595B44700E47BAF /* HelperClient.swift */,
CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */, CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */,
CAC281D9259F985100B8AB0B /* InstallationStep.swift */,
CA9FF8862595607900E47BAF /* InstalledXcode.swift */, CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */, CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */,
E89342F925EDCC17007CF557 /* NotificationManager.swift */, E89342F925EDCC17007CF557 /* NotificationManager.swift */,
@ -863,7 +860,6 @@
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */, CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */,
CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */, CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */,
CABFA9C32592EEEA00380FEE /* Downloads.swift in Sources */, CABFA9C32592EEEA00380FEE /* Downloads.swift in Sources */,
CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */,
E8CBDB8B27AE02FF00B22292 /* ExperiementsPreferencePane.swift in Sources */, E8CBDB8B27AE02FF00B22292 /* ExperiementsPreferencePane.swift in Sources */,
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */, E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */,
CA378F992466567600A58CE0 /* AppState.swift in Sources */, CA378F992466567600A58CE0 /* AppState.swift in Sources */,
@ -1427,8 +1423,6 @@
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/xcodereleases/data"; repositoryURL = "https://github.com/xcodereleases/data";
requirement = { requirement = {
kind = revision;
revision = a43ad89e536d7a3da525fcc23fb182c37b756ecc;
branch = main; branch = main;
kind = branch; kind = branch;
}; };

View file

@ -23,7 +23,7 @@
"package": "XcodeReleases", "package": "XcodeReleases",
"repositoryURL": "https://github.com/xcodereleases/data", "repositoryURL": "https://github.com/xcodereleases/data",
"state": { "state": {
"branch": null, "branch": "main",
"revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc", "revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc",
"version": null "version": null
} }
@ -69,8 +69,8 @@
"repositoryURL": "https://github.com/mxcl/Path.swift", "repositoryURL": "https://github.com/mxcl/Path.swift",
"state": { "state": {
"branch": null, "branch": null,
"revision": "9c6f807b0a76be0e27aecc908bc6f173400d839e", "revision": "8e355c28e9393c42e58b18c54cace2c42c98a616",
"version": "1.4.0" "version": "1.4.1"
} }
}, },
{ {

View file

@ -490,7 +490,7 @@ extension AppState {
// MARK: - // MARK: -
func setInstallationStep(of version: Version, to step: InstallationStep) { func setInstallationStep(of version: Version, to step: XcodeInstallationStep) {
DispatchQueue.main.async { DispatchQueue.main.async {
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return } guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return }
self.allXcodes[index].installState = .installing(step) self.allXcodes[index].installState = .installing(step)
@ -499,14 +499,13 @@ extension AppState {
Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal) Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal)
} }
} }
func setInstallationStep(of runtime: DownloadableRuntime, to step: InstallationStep) { func setInstallationStep(of runtime: DownloadableRuntime, to step: RuntimeInstallationStep) {
DispatchQueue.main.async { DispatchQueue.main.async {
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return } guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
self.downloadableRuntimes[index].installState = .installing(step) self.downloadableRuntimes[index].installState = .installing(step)
// let xcode = self.allXcodes[index] Current.notificationManager.scheduleNotification(title: runtime.name, body: step.description, category: .normal)
// Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal)
} }
} }
} }

View file

@ -47,7 +47,12 @@ extension AppState {
func downloadRuntime(runtime: DownloadableRuntime) { func downloadRuntime(runtime: DownloadableRuntime) {
Task { Task {
try? await downloadRunTimeFull(runtime: runtime) do {
try await downloadRunTimeFull(runtime: runtime)
}
catch {
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
}
} }
// self.runtimePublishers[runtime.identifier] = downloadRunTimeFull(runtime: runtime) // self.runtimePublishers[runtime.identifier] = downloadRunTimeFull(runtime: runtime)
@ -78,33 +83,20 @@ extension AppState {
let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2 let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2
Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)") Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)")
let url = try await self.downloadRuntime(for: runtime, downloader: downloader, progressChanged: { [unowned self] progress in
DispatchQueue.main.async {
self.setInstallationStep(of: runtime, to: .downloading(progress: progress))
}
}).async()
return validateADCSession(path: runtime.downloadPath) Logger.appState.debug("Done downloading: \(url)")
.flatMap { _ in //self.setInstallationStep(of: runtime, to: .downloading(progress: progress))
// we shouldn't have to be authenticated to download runtimes switch runtime.contentType {
let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2 case .package:
Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)") try await self.installFromPackage(dmgURL: url, runtime: runtime)
case .diskImage:
return self.downloadRuntime(for: runtime, downloader: downloader, progressChanged: { [unowned self] progress in try await self.installFromImage(dmgURL: url)
DispatchQueue.main.async { }
self.setInstallationStep(of: runtime, to: .downloading(progress: progress))
}
})
.map { return (runtime, $0) }
}
.flatMap { runtime, url -> AnyPublisher<URL, Error> in
switch runtime.contentType {
case .package:
return self.installFromPackage(dmgURL: url, runtime: runtime)
case .diskImage:
return self.installFromImage(dmgURL: url)
}
}
.map { url in
// Done deleting
Logger.appState.debug("URL: \(url)")
}
.eraseToAnyPublisher()
} }
func downloadRuntime(for runtime: DownloadableRuntime, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> { func downloadRuntime(for runtime: DownloadableRuntime, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> {
@ -142,7 +134,7 @@ extension AppState {
progressChanged: progressChanged) progressChanged: progressChanged)
case .urlSession: case .urlSession:
// TODO: Support runtime download via URL Session
return Just(runtime.url) return Just(runtime.url)
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -171,53 +163,56 @@ extension AppState {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path, progressChanged: @escaping (Progress) -> Void) async -> URL {
public func installFromImage(dmgURL: URL) async throws {
try? self.runtimeService.installRuntimeImage(dmgURL: dmgURL)
} }
public func installFromImage(dmgURL: URL) -> AnyPublisher<URL, Error> { public func installFromPackage(dmgURL: URL, runtime: DownloadableRuntime) async throws {
try? self.runtimeService.installRuntimeImage(dmgURL: dmgURL)
return Just(dmgURL)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
public func installFromPackage(dmgURL: URL, runtime: DownloadableRuntime) -> AnyPublisher<URL, Error> {
Logger.appState.info("Mounting DMG") Logger.appState.info("Mounting DMG")
Task {
do {
let mountedUrl = try await self.runtimeService.mountDMG(dmgUrl: dmgURL)
// 2-Get the first path under the mounted path, should be a .pkg
let pkgPath = Path(url: mountedUrl)!.ls().first!
try Path.xcodesCaches.mkdir().setCurrentUserAsOwner()
let expandedPkgPath = Path.xcodesCaches/runtime.identifier
//try expandedPkgPath.mkdir()
Logger.appState.info("PKG Path: \(pkgPath)")
Logger.appState.info("Expanded PKG Path: \(expandedPkgPath)")
//try? Current.files.removeItem(at: expandedPkgPath.url)
// 5-Expand (not install) the pkg to temporary path
try await self.runtimeService.expand(pkgPath: pkgPath, expandedPkgPath: expandedPkgPath)
//try await self.runtimeService.unmountDMG(mountedURL: mountedUrl)
} catch {
Logger.appState.error("Error installing runtime: \(error.localizedDescription)")
}
}
do {
let mountedUrl = try await self.runtimeService.mountDMG(dmgUrl: dmgURL)
return Just(dmgURL)
.setFailureType(to: Error.self) // 2-Get the first path under the mounted path, should be a .pkg
.eraseToAnyPublisher() let pkgPath = Path(url: mountedUrl)!.ls().first!
try Path.xcodesCaches.mkdir().setCurrentUserAsOwner()
let expandedPkgPath = Path.xcodesCaches/runtime.identifier
//try expandedPkgPath.mkdir()
Logger.appState.info("PKG Path: \(pkgPath)")
Logger.appState.info("Expanded PKG Path: \(expandedPkgPath)")
//try? Current.files.removeItem(at: expandedPkgPath.url)
// 5-Expand (not install) the pkg to temporary path
try await self.runtimeService.expand(pkgPath: pkgPath, expandedPkgPath: expandedPkgPath)
//try await self.runtimeService.unmountDMG(mountedURL: mountedUrl)
} catch {
Logger.appState.error("Error installing runtime: \(error.localizedDescription)")
}
}
}
extension AnyPublisher {
func async() async throws -> Output {
try await withCheckedThrowingContinuation { continuation in
var cancellable: AnyCancellable?
cancellable = first()
.sink { result in
switch result {
case .finished:
break
case let .failure(error):
continuation.resume(throwing: error)
}
cancellable?.cancel()
} receiveValue: { value in
continuation.resume(with: .success(value))
}
}
} }
} }

View file

@ -112,100 +112,73 @@ public struct Shell {
return (progress, publisher) return (progress, publisher)
} }
public var downloadWithAria2Async: (Path, URL, Path, [HTTPCookie]) async throws -> Progress = { aria2Path, url, destination, cookies in // public var downloadWithAria2Async: (Path, URL, Path, [HTTPCookie]) async throws -> Progress = { aria2Path, url, destination, cookies in
let process = Process() // let process = Process()
process.executableURL = aria2Path.url // process.executableURL = aria2Path.url
process.arguments = [ // process.arguments = [
"--header=Cookie: \(cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; "))", // "--header=Cookie: \(cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; "))",
"--max-connection-per-server=16", // "--max-connection-per-server=16",
"--split=16", // "--split=16",
"--summary-interval=1", // "--summaraasdy-interval=1",
"--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process // "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process
"--dir=\(destination.parent.string)", // "--dir=\(destination.parent.string)",
"--out=\(destination.basename())", // "--out=\(destination.basename())",
"--human-readable=false", // sets the output to use bytes instead of formatting // "--human-readable=false", // sets the output to use bytes instead of formatting
url.absoluteString, // url.absoluteString,
] // ]
let stdOutPipe = Pipe() // let stdOutPipe = Pipe()
process.standardOutput = stdOutPipe // process.standardOutput = stdOutPipe
let stdErrPipe = Pipe() // let stdErrPipe = Pipe()
process.standardError = stdErrPipe // process.standardError = stdErrPipe
//
var progress = Progress() // var progress = Progress()
progress.kind = .file // progress.kinasdas
progress.fileOperationKind = .downloading // progress.fileOperationKind = .downloadingasdfasd
//
let observer = NotificationCenter.default.addObserver( // let observer = NotificationCenter.default.addObserver(
forName: .NSFileHandleDataAvailable, // forName: .NSFileHandleDataAvailable,
object: nil, // object: nil,
queue: OperationQueue.main // queue: OperationQueue.main
) { note in // ) { note in
guard // guard
// This should always be the case for Notification.Name.NSFileHandleDataAvailable // // This should always be the case for Notification.Name.NSFileHandleDataAvailable
let handle = note.object as? FileHandle, // let handle = note.object as? FileHandle,
handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading // handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading
else { return } // else { return }
//
defer { handle.waitForDataInBackgroundAndNotify() } // defer { handle.waitForDataInBackgroundAndNotify() }
//
let string = String(decoding: handle.availableData, as: UTF8.self) // let string = String(decoding: handle.availableData, as: UTF8.self)
//
progress.updateFromAria2(string: string) // progress.updateFromAria2(string: string)
} // }
//
stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() // stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() // stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
//
do { // do {
//
defer { // defer {
//DispatchQueue.global(qos: .default).async { // //DispatchQueue.global(qos: .default).async {
process.waitUntilExit()
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
if let aria2cError = Aria2CError(exitStatus: process.terminationStatus) {
throw aria2cError
} else {
throw ProcessExecutionError(process: process, standardOutput: "", standardError: "")
}
}
return
// }
}
try process.run()
} catch {
throw error
}
// let publisher = Deferred {
// Future<Void, Error> { promise in
// DispatchQueue.global(qos: .default).async {
// process.waitUntilExit() // process.waitUntilExit()
// //
// NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil) // NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
// //
// guard process.terminationReason == .exit, process.terminationStatus == 0 else { // guard process.terminationReason == .exit, process.terminationStatus == 0 else {
// if let aria2cError = Aria2CError(exitStatus: process.terminationStatus) { // if let aria2cError = Aria2CError(exitStatus: process.terminationStatus) {
// return promise(.failure(aria2cError)) // throw aria2cError
// } else { // } else {
// return promise(.failure(ProcessExecutionError(process: process, standardOutput: "", standardError: ""))) // throw ProcessExecutionError(process: process, standardOutput: "", standardError: "")
// } // }
// } // }
// promise(.success(())) // return
// } // }
// } // }
// try process.run()
// } catch {
// throw error
// } // }
// .handleEvents(receiveCancel: { // }
// process.terminate()
// NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
// })
// .eraseToAnyPublisher()
//
// return (progress, publisher)
}
public var unxipExperiment: (URL) -> AnyPublisher<ProcessOutput, Error> = { url in public var unxipExperiment: (URL) -> AnyPublisher<ProcessOutput, Error> = { url in
let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)! let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)!

View file

@ -1,45 +0,0 @@
//import Foundation
//
///// A numbered step
//enum InstallationStep: Equatable, CustomStringConvertible {
// case downloading(progress: Progress)
// case unarchiving
// case moving(destination: String)
// case trashingArchive
// case checkingSecurity
// case finishing
//
// var description: String {
// "(\(stepNumber)/\(stepCount)) \(message)"
// }
//
// var message: String {
// switch self {
// case .downloading:
// return localizeString("Downloading")
// case .unarchiving:
// return localizeString("Unarchiving")
// case .moving(let destination):
// return String(format: localizeString("Moving"), destination)
// case .trashingArchive:
// return localizeString("TrashingArchive")
// case .checkingSecurity:
// return localizeString("CheckingSecurity")
// case .finishing:
// return localizeString("Finishing")
// }
// }
//
// var stepNumber: Int {
// switch self {
// case .downloading: return 1
// case .unarchiving: return 2
// case .moving: return 3
// case .trashingArchive: return 4
// case .checkingSecurity: return 5
// case .finishing: return 6
// }
// }
//
// var stepCount: Int { 6 }
//}

View file

@ -4,6 +4,8 @@ import os.log
import Path import Path
import XcodesKit import XcodesKit
public typealias ProcessOutput = (status: Int32, out: String, err: String)
extension Process { extension Process {
@discardableResult @discardableResult
static func run(_ executable: any Pathish, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher<ProcessOutput, Error> { static func run(_ executable: any Pathish, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher<ProcessOutput, Error> {

View file

@ -4,7 +4,7 @@ import XcodesKit
enum XcodeInstallState: Equatable { enum XcodeInstallState: Equatable {
case notInstalled case notInstalled
case installing(InstallationStep) case installing(XcodeInstallationStep)
case installed(Path) case installed(Path)
var notInstalled: Bool { var notInstalled: Bool {

View file

@ -2,7 +2,7 @@ import SwiftUI
import XcodesKit import XcodesKit
struct InstallationStepDetailView: View { struct InstallationStepDetailView: View {
let installationStep: InstallationStep let installationStep: XcodeInstallationStep
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {

View file

@ -2,7 +2,7 @@ import SwiftUI
import XcodesKit import XcodesKit
struct InstallationStepRowView: View { struct InstallationStepRowView: View {
let installationStep: InstallationStep let installationStep: XcodeInstallationStep
let highlighted: Bool let highlighted: Bool
let cancel: () -> Void let cancel: () -> Void

View file

@ -1,4 +1,4 @@
{\rtf1\ansi\ansicpg1252\cocoartf2709 {\rtf1\ansi\ansicpg1252\cocoartf2758
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;} \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
{\colortbl;\red255\green255\blue255;} {\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;} {\*\expandedcolortbl;;}

View file

@ -1,7 +0,0 @@
import Foundation
public struct Environment {
public var shell = Shell()
}
public var Current = Environment()

View file

@ -0,0 +1,34 @@
//
// RuntimeInstallState.swift
//
//
// Created by Matt Kiazyk on 2023-11-23.
//
import Foundation
import Path
public enum RuntimeInstallState: Equatable {
case notInstalled
case installing(RuntimeInstallationStep)
case installed(Path)
var notInstalled: Bool {
switch self {
case .notInstalled: return true
default: return false
}
}
var installing: Bool {
switch self {
case .installing: return true
default: return false
}
}
var installed: Bool {
switch self {
case .installed: return true
default: return false
}
}
}

View file

@ -0,0 +1,51 @@
//
// RuntimeInstallationStep.swift
//
//
// Created by Matt Kiazyk on 2023-11-23.
//
import Foundation
public enum RuntimeInstallationStep: Equatable, CustomStringConvertible {
case downloading(progress: Progress)
case unarchiving
case moving(destination: String)
case trashingArchive
case checkingSecurity
case finishing
public var description: String {
"(\(stepNumber)/\(stepCount)) \(message)"
}
public var message: String {
switch self {
case .downloading:
return localizeString("Downloading")
case .unarchiving:
return localizeString("Unarchiving")
case .moving(let destination):
return String(format: localizeString("Moving"), destination)
case .trashingArchive:
return localizeString("TrashingArchive")
case .checkingSecurity:
return localizeString("CheckingSecurity")
case .finishing:
return localizeString("Finishing")
}
}
public var stepNumber: Int {
switch self {
case .downloading: return 1
case .unarchiving: return 2
case .moving: return 3
case .trashingArchive: return 4
case .checkingSecurity: return 5
case .finishing: return 6
}
}
public var stepCount: Int { 6 }
}

View file

@ -29,7 +29,7 @@ public struct DownloadableRuntime: Codable {
} }
// dynamically updated - not decoded // dynamically updated - not decoded
public var installState: InstallState = .notInstalled public var installState: RuntimeInstallState = .notInstalled
public var sdkBuildUpdate: String? public var sdkBuildUpdate: String?
enum CodingKeys: CodingKey { enum CodingKeys: CodingKey {

View file

@ -8,9 +8,9 @@
import Foundation import Foundation
import Path import Path
public enum InstallState: Equatable { public enum XcodeInstallState: Equatable {
case notInstalled case notInstalled
case installing(InstallationStep) case installing(XcodeInstallationStep)
case installed(Path) case installed(Path)
var notInstalled: Bool { var notInstalled: Bool {

View file

@ -8,7 +8,7 @@
import Foundation import Foundation
// A numbered step // A numbered step
public enum InstallationStep: Equatable, CustomStringConvertible { public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
case downloading(progress: Progress) case downloading(progress: Progress)
case unarchiving case unarchiving
case moving(destination: String) case moving(destination: String)
@ -50,6 +50,7 @@ public enum InstallationStep: Equatable, CustomStringConvertible {
public var stepCount: Int { 6 } public var stepCount: Int { 6 }
} }
func localizeString(_ key: String, comment: String = "") -> String { func localizeString(_ key: String, comment: String = "") -> String {
if #available(macOS 12, *) { if #available(macOS 12, *) {
return String(localized: String.LocalizationValue(key)) return String(localized: String.LocalizationValue(key))

View file

@ -1,7 +1,7 @@
import Foundation import Foundation
import Path import Path
public struct Shell { public struct XcodesShell {
public var installedRuntimes: () async throws -> ProcessOutput = { public var installedRuntimes: () async throws -> ProcessOutput = {
try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "list", "-j") try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "list", "-j")
} }

View file

@ -0,0 +1,7 @@
import Foundation
public struct XcodesKitEnvironment {
public var shell = XcodesShell()
}
public var Current = XcodesKitEnvironment()

View file

@ -4,6 +4,8 @@ import CombineExpectations
import Path import Path
import Version import Version
import XCTest import XCTest
import XcodesKit
@testable import Xcodes @testable import Xcodes
class AppStateTests: XCTestCase { class AppStateTests: XCTestCase {

View file

@ -2,8 +2,8 @@ import Combine
import Foundation import Foundation
@testable import Xcodes @testable import Xcodes
extension Environment { extension Xcodes.Environment {
static var mock = Environment( static var mock = Xcodes.Environment(
shell: .mock, shell: .mock,
files: .mock, files: .mock,
network: .mock, network: .mock,