From 6ffce23616a055889b526c676333eade182d89e2 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Thu, 23 Nov 2023 10:37:41 -0600 Subject: [PATCH] more WIP --- Xcodes.xcodeproj/project.pbxproj | 6 - .../xcshareddata/swiftpm/Package.resolved | 6 +- Xcodes/Backend/AppState+Install.swift | 7 +- Xcodes/Backend/AppState+Runtimes.swift | 137 ++++++++--------- Xcodes/Backend/Environment.swift | 141 +++++++----------- Xcodes/Backend/InstallationStep.swift | 45 ------ Xcodes/Backend/Process.swift | 2 + Xcodes/Backend/XcodeInstallState.swift | 2 +- .../InfoPane/InstallationStepDetailView.swift | 2 +- .../XcodeList/InstallationStepRowView.swift | 2 +- Xcodes/Resources/Licenses.rtf | 2 +- .../Sources/XcodesKit/Environment.swift | 7 - .../Models/Runtimes/RuntimeInstallState.swift | 34 +++++ .../Runtimes/RuntimeInstallationStep.swift | 51 +++++++ .../XcodesKit/Models/Runtimes/Runtimes.swift | 2 +- ...allState.swift => XcodeInstallState.swift} | 4 +- ...Step.swift => XcodeInstallationStep.swift} | 3 +- .../Shell/{Shell.swift => XcodesShell.swift} | 2 +- .../XcodesKit/XcodesKitEnvironment.swift | 7 + XcodesTests/AppStateTests.swift | 2 + XcodesTests/Environment+Mock.swift | 4 +- 21 files changed, 237 insertions(+), 231 deletions(-) delete mode 100644 Xcodes/Backend/InstallationStep.swift delete mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift rename Xcodes/XcodesKit/Sources/XcodesKit/Models/{InstallState.swift => XcodeInstallState.swift} (86%) rename Xcodes/XcodesKit/Sources/XcodesKit/Models/{InstallationStep.swift => XcodeInstallationStep.swift} (95%) rename Xcodes/XcodesKit/Sources/XcodesKit/Shell/{Shell.swift => XcodesShell.swift} (97%) create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/XcodesKitEnvironment.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 86eb5b4..17f8687 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -79,7 +79,6 @@ CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */; }; CAC28188259EE27200B8AB0B /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = CAC28187259EE27200B8AB0B /* CombineExpectations */; }; 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 */; }; CAC281E7259FA45A00B8AB0B /* Environment+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281E6259FA45A00B8AB0B /* Environment+Mock.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 = ""; }; CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+InfoPlistValues.swift"; sourceTree = ""; }; CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingProgressIndicator.swift; sourceTree = ""; }; - CAC281D9259F985100B8AB0B /* InstallationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStep.swift; sourceTree = ""; }; CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+XcodesTests.swift"; sourceTree = ""; }; CAC281E6259FA45A00B8AB0B /* Environment+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Mock.swift"; sourceTree = ""; }; CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperInstallState.swift; sourceTree = ""; }; @@ -482,7 +480,6 @@ CABFA9AC2592EEE900380FEE /* Foundation.swift */, CA9FF9352595B44700E47BAF /* HelperClient.swift */, CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */, - CAC281D9259F985100B8AB0B /* InstallationStep.swift */, CA9FF8862595607900E47BAF /* InstalledXcode.swift */, CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */, E89342F925EDCC17007CF557 /* NotificationManager.swift */, @@ -863,7 +860,6 @@ CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */, CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */, CABFA9C32592EEEA00380FEE /* Downloads.swift in Sources */, - CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */, E8CBDB8B27AE02FF00B22292 /* ExperiementsPreferencePane.swift in Sources */, E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */, CA378F992466567600A58CE0 /* AppState.swift in Sources */, @@ -1427,8 +1423,6 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/xcodereleases/data"; requirement = { - kind = revision; - revision = a43ad89e536d7a3da525fcc23fb182c37b756ecc; branch = main; kind = branch; }; diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 25d8d5b..a08a9b2 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,7 +23,7 @@ "package": "XcodeReleases", "repositoryURL": "https://github.com/xcodereleases/data", "state": { - "branch": null, + "branch": "main", "revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc", "version": null } @@ -69,8 +69,8 @@ "repositoryURL": "https://github.com/mxcl/Path.swift", "state": { "branch": null, - "revision": "9c6f807b0a76be0e27aecc908bc6f173400d839e", - "version": "1.4.0" + "revision": "8e355c28e9393c42e58b18c54cace2c42c98a616", + "version": "1.4.1" } }, { diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index dd8eec6..936bb6e 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -490,7 +490,7 @@ extension AppState { // MARK: - - func setInstallationStep(of version: Version, to step: InstallationStep) { + func setInstallationStep(of version: Version, to step: XcodeInstallationStep) { DispatchQueue.main.async { guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return } self.allXcodes[index].installState = .installing(step) @@ -499,14 +499,13 @@ extension AppState { 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 { guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return } self.downloadableRuntimes[index].installState = .installing(step) - // let xcode = self.allXcodes[index] - // Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal) + Current.notificationManager.scheduleNotification(title: runtime.name, body: step.description, category: .normal) } } } diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index 092accc..f8b6489 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -47,7 +47,12 @@ extension AppState { func downloadRuntime(runtime: DownloadableRuntime) { 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) @@ -78,33 +83,20 @@ extension AppState { let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2 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) - .flatMap { _ in - // we shouldn't have to be authenticated to download runtimes - let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2 - Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)") - - return self.downloadRuntime(for: runtime, downloader: downloader, progressChanged: { [unowned self] progress in - DispatchQueue.main.async { - self.setInstallationStep(of: runtime, to: .downloading(progress: progress)) - } - }) - .map { return (runtime, $0) } - } - .flatMap { runtime, url -> AnyPublisher 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() + Logger.appState.debug("Done downloading: \(url)") + //self.setInstallationStep(of: runtime, to: .downloading(progress: progress)) + switch runtime.contentType { + case .package: + try await self.installFromPackage(dmgURL: url, runtime: runtime) + case .diskImage: + try await self.installFromImage(dmgURL: url) + } } func downloadRuntime(for runtime: DownloadableRuntime, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher { @@ -142,7 +134,7 @@ extension AppState { progressChanged: progressChanged) case .urlSession: - + // TODO: Support runtime download via URL Session return Just(runtime.url) .setFailureType(to: Error.self) .eraseToAnyPublisher() @@ -171,53 +163,56 @@ extension AppState { .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 { - - - try? self.runtimeService.installRuntimeImage(dmgURL: dmgURL) - - - return Just(dmgURL) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - - } - - public func installFromPackage(dmgURL: URL, runtime: DownloadableRuntime) -> AnyPublisher { + public func installFromPackage(dmgURL: URL, runtime: DownloadableRuntime) async throws { 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)") - } - - } - - - return Just(dmgURL) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + 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)") + } + } +} + +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)) + } + } } - } diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index 8b7da37..d84ad26 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -112,100 +112,73 @@ public struct Shell { return (progress, publisher) } - public var downloadWithAria2Async: (Path, URL, Path, [HTTPCookie]) async throws -> Progress = { aria2Path, url, destination, cookies in - let process = Process() - process.executableURL = aria2Path.url - process.arguments = [ - "--header=Cookie: \(cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; "))", - "--max-connection-per-server=16", - "--split=16", - "--summary-interval=1", - "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process - "--dir=\(destination.parent.string)", - "--out=\(destination.basename())", - "--human-readable=false", // sets the output to use bytes instead of formatting - url.absoluteString, - ] - let stdOutPipe = Pipe() - process.standardOutput = stdOutPipe - let stdErrPipe = Pipe() - process.standardError = stdErrPipe - - var progress = Progress() - progress.kind = .file - progress.fileOperationKind = .downloading - - let observer = NotificationCenter.default.addObserver( - forName: .NSFileHandleDataAvailable, - object: nil, - queue: OperationQueue.main - ) { note in - guard - // This should always be the case for Notification.Name.NSFileHandleDataAvailable - let handle = note.object as? FileHandle, - handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading - else { return } - - defer { handle.waitForDataInBackgroundAndNotify() } - - let string = String(decoding: handle.availableData, as: UTF8.self) - - progress.updateFromAria2(string: string) - } - - stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() - stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() - - do { - - defer { - //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 { promise in -// DispatchQueue.global(qos: .default).async { +// public var downloadWithAria2Async: (Path, URL, Path, [HTTPCookie]) async throws -> Progress = { aria2Path, url, destination, cookies in +// let process = Process() +// process.executableURL = aria2Path.url +// process.arguments = [ +// "--header=Cookie: \(cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; "))", +// "--max-connection-per-server=16", +// "--split=16", +// "--summaraasdy-interval=1", +// "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process +// "--dir=\(destination.parent.string)", +// "--out=\(destination.basename())", +// "--human-readable=false", // sets the output to use bytes instead of formatting +// url.absoluteString, +// ] +// let stdOutPipe = Pipe() +// process.standardOutput = stdOutPipe +// let stdErrPipe = Pipe() +// process.standardError = stdErrPipe +// +// var progress = Progress() +// progress.kinasdas +// progress.fileOperationKind = .downloadingasdfasd +// +// let observer = NotificationCenter.default.addObserver( +// forName: .NSFileHandleDataAvailable, +// object: nil, +// queue: OperationQueue.main +// ) { note in +// guard +// // This should always be the case for Notification.Name.NSFileHandleDataAvailable +// let handle = note.object as? FileHandle, +// handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading +// else { return } +// +// defer { handle.waitForDataInBackgroundAndNotify() } +// +// let string = String(decoding: handle.availableData, as: UTF8.self) +// +// progress.updateFromAria2(string: string) +// } +// +// stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() +// stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() +// +// do { +// +// defer { +// //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) { -// return promise(.failure(aria2cError)) +// throw aria2cError // } 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 = { url in let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)! diff --git a/Xcodes/Backend/InstallationStep.swift b/Xcodes/Backend/InstallationStep.swift deleted file mode 100644 index 9519cf5..0000000 --- a/Xcodes/Backend/InstallationStep.swift +++ /dev/null @@ -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 } -//} diff --git a/Xcodes/Backend/Process.swift b/Xcodes/Backend/Process.swift index 77935df..b3e5a64 100644 --- a/Xcodes/Backend/Process.swift +++ b/Xcodes/Backend/Process.swift @@ -4,6 +4,8 @@ import os.log import Path import XcodesKit +public typealias ProcessOutput = (status: Int32, out: String, err: String) + extension Process { @discardableResult static func run(_ executable: any Pathish, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher { diff --git a/Xcodes/Backend/XcodeInstallState.swift b/Xcodes/Backend/XcodeInstallState.swift index b23b75e..a289bb5 100644 --- a/Xcodes/Backend/XcodeInstallState.swift +++ b/Xcodes/Backend/XcodeInstallState.swift @@ -4,7 +4,7 @@ import XcodesKit enum XcodeInstallState: Equatable { case notInstalled - case installing(InstallationStep) + case installing(XcodeInstallationStep) case installed(Path) var notInstalled: Bool { diff --git a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift index e10b193..1ed801a 100644 --- a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift +++ b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift @@ -2,7 +2,7 @@ import SwiftUI import XcodesKit struct InstallationStepDetailView: View { - let installationStep: InstallationStep + let installationStep: XcodeInstallationStep var body: some View { VStack(alignment: .leading, spacing: 0) { diff --git a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift index ef716af..3bf7db5 100644 --- a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift +++ b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift @@ -2,7 +2,7 @@ import SwiftUI import XcodesKit struct InstallationStepRowView: View { - let installationStep: InstallationStep + let installationStep: XcodeInstallationStep let highlighted: Bool let cancel: () -> Void diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index d89a291..179cdf4 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -1,4 +1,4 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2709 +{\rtf1\ansi\ansicpg1252\cocoartf2758 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;} {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift deleted file mode 100644 index a988ce9..0000000 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -public struct Environment { - public var shell = Shell() -} - -public var Current = Environment() diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift new file mode 100644 index 0000000..25bc5bc --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift @@ -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 + } + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift new file mode 100644 index 0000000..21946a1 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift @@ -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 } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift index 2ab3362..9e9e370 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift @@ -29,7 +29,7 @@ public struct DownloadableRuntime: Codable { } // dynamically updated - not decoded - public var installState: InstallState = .notInstalled + public var installState: RuntimeInstallState = .notInstalled public var sdkBuildUpdate: String? enum CodingKeys: CodingKey { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallState.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallState.swift similarity index 86% rename from Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallState.swift rename to Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallState.swift index 8aa6b1e..0f824a5 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallState.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallState.swift @@ -8,9 +8,9 @@ import Foundation import Path -public enum InstallState: Equatable { +public enum XcodeInstallState: Equatable { case notInstalled - case installing(InstallationStep) + case installing(XcodeInstallationStep) case installed(Path) var notInstalled: Bool { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallationStep.swift similarity index 95% rename from Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift rename to Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallationStep.swift index e33528f..8d5513d 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallationStep.swift @@ -8,7 +8,7 @@ import Foundation // A numbered step -public enum InstallationStep: Equatable, CustomStringConvertible { +public enum XcodeInstallationStep: Equatable, CustomStringConvertible { case downloading(progress: Progress) case unarchiving case moving(destination: String) @@ -50,6 +50,7 @@ public enum InstallationStep: Equatable, CustomStringConvertible { public var stepCount: Int { 6 } } + func localizeString(_ key: String, comment: String = "") -> String { if #available(macOS 12, *) { return String(localized: String.LocalizationValue(key)) diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift similarity index 97% rename from Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift rename to Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift index 81157d7..c5760b3 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift @@ -1,7 +1,7 @@ import Foundation import Path -public struct Shell { +public struct XcodesShell { public var installedRuntimes: () async throws -> ProcessOutput = { try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "list", "-j") } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/XcodesKitEnvironment.swift b/Xcodes/XcodesKit/Sources/XcodesKit/XcodesKitEnvironment.swift new file mode 100644 index 0000000..c6a5243 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/XcodesKitEnvironment.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct XcodesKitEnvironment { + public var shell = XcodesShell() +} + +public var Current = XcodesKitEnvironment() diff --git a/XcodesTests/AppStateTests.swift b/XcodesTests/AppStateTests.swift index ce38607..4be9ca3 100644 --- a/XcodesTests/AppStateTests.swift +++ b/XcodesTests/AppStateTests.swift @@ -4,6 +4,8 @@ import CombineExpectations import Path import Version import XCTest +import XcodesKit + @testable import Xcodes class AppStateTests: XCTestCase { diff --git a/XcodesTests/Environment+Mock.swift b/XcodesTests/Environment+Mock.swift index 8755da5..f030d79 100644 --- a/XcodesTests/Environment+Mock.swift +++ b/XcodesTests/Environment+Mock.swift @@ -2,8 +2,8 @@ import Combine import Foundation @testable import Xcodes -extension Environment { - static var mock = Environment( +extension Xcodes.Environment { + static var mock = Xcodes.Environment( shell: .mock, files: .mock, network: .mock,