diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 56d06e7..17491bf 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -1054,7 +1054,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.10.0; - PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp; + PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp; PRODUCT_NAME = Xcodes; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -1286,7 +1286,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; - DEVELOPMENT_TEAM = PBH8V487HB; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Xcodes/Resources/Info.plist; @@ -1295,7 +1295,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.10.0; - PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp; + PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp; PRODUCT_NAME = Xcodes; SWIFT_VERSION = 5.0; }; @@ -1310,7 +1310,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; - DEVELOPMENT_TEAM = PBH8V487HB; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Xcodes/Resources/Info.plist; @@ -1319,7 +1319,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.10.0; - PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp; + PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp; PRODUCT_NAME = Xcodes; SWIFT_VERSION = 5.0; }; @@ -1417,8 +1417,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/xcodereleases/data"; requirement = { - kind = revision; - revision = b47228c688b608e34b3b84079ab6052a24c7a981; + branch = main; + kind = branch; }; }; CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */ = { diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index abfd4fe..74dc3a7 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "package": "XcodeReleases", "repositoryURL": "https://github.com/xcodereleases/data", "state": { - "branch": null, - "revision": "b47228c688b608e34b3b84079ab6052a24c7a981", + "branch": "main", + "revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc", "version": null } }, diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index a889e69..70f82d0 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -466,6 +466,15 @@ extension AppState { let xcode = self.allXcodes[index] Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal) } + }w func setInstallationStep(of runtime: DownloadableRuntime, to step: InstallationStep) { + 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) + } } } diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index a07f798..2f1c568 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -1,12 +1,27 @@ import Foundation import XcodesKit import OSLog +import Combine +import Path +import AppleAPI extension AppState { func updateDownloadableRuntimes() { Task { do { - let runtimes = try await self.runtimeService.downloadableRuntimes().downloadables + + let downloadableRuntimes = try await self.runtimeService.downloadableRuntimes() + let runtimes = downloadableRuntimes.downloadables.map { runtime in + var updatedRuntime = runtime + + // This loops through and matches up the simulatorVersion to the mappings + let simulatorBuildUpdate = downloadableRuntimes.sdkToSimulatorMappings.first { SDKToSimulatorMapping in + SDKToSimulatorMapping.simulatorBuildUpdate == runtime.simulatorVersion.buildUpdate + } + updatedRuntime.sdkBuildUpdate = simulatorBuildUpdate?.sdkBuildUpdate + return updatedRuntime + } + DispatchQueue.main.async { self.downloadableRuntimes = runtimes } @@ -29,4 +44,111 @@ extension AppState { } } } + + func downloadRuntime(runtime: DownloadableRuntime) { + self.runtimePublishers[runtime.identifier] = downloadRunTimeFull(runtime: runtime) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { [unowned self] completion in + self.runtimePublishers[runtime.identifier] = nil + if case let .failure(error) = completion { +// // Prevent setting the app state error if it is an invalid session, we will present the sign in view instead +// if error as? AuthenticationError != .invalidSession { +// self.error = error +// self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription) +// } +// if let index = self.allXcodes.firstIndex(where: { $0.id == id }) { +// self.allXcodes[index].installState = .notInstalled +// } + } + }, + receiveValue: { _ in } + ) + } + + func downloadRunTimeFull(runtime: DownloadableRuntime) -> AnyPublisher<(DownloadableRuntime, URL), Error> { + // gets a proper cookie for runtimes + + 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) } + } + .eraseToAnyPublisher() + } + + func downloadRuntime(for runtime: DownloadableRuntime, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher { + // Check to see if the dmg is in the expected path in case it was downloaded but failed to install + + // call https://developerservices2.apple.com/services/download?path=/Developer_Tools/watchOS_10_beta/watchOS_10_beta_Simulator_Runtime.dmg 1st to get cookie + // use runtime.url for final with cookies + + // Check to see if the archive is in the expected path in case it was downloaded but failed to install + let expectedRuntimePath = Path.xcodesApplicationSupport/"\(runtime.name).\(runtime.name.suffix(fromLast: "."))" + // aria2 downloads directly to the destination (instead of into /tmp first) so we need to make sure that the download isn't incomplete + let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2") + var aria2DownloadIsIncomplete = false + if case .aria2 = downloader, aria2DownloadMetadataPath.exists { + aria2DownloadIsIncomplete = true + } + if Current.files.fileExistsAtPath(expectedRuntimePath.string), aria2DownloadIsIncomplete == false { + Logger.appState.info("Found existing runtime that will be used for installation at \(expectedRuntimePath).") + return Just(expectedRuntimePath.url) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + else { +// let destination = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).\(availableXcode.filename.suffix(fromLast: "."))" + switch downloader { + case .aria2: + let aria2Path = Path(url: Bundle.main.url(forAuxiliaryExecutable: "aria2c")!)! + return downloadRuntimeWithAria2( + runtime, + to: expectedRuntimePath, + aria2Path: aria2Path, + progressChanged: progressChanged) +// return downloadXcodeWithAria2( +// availableXcode, +// to: destination, +// aria2Path: aria2Path, +// progressChanged: progressChanged +// ) + case .urlSession: + + return Just(runtime.url) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + +// return downloadXcodeWithURLSession( +// availableXcode, +// to: destination, +// progressChanged: progressChanged +// ) + } + } + + } + + public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher { + let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: runtime.url) ?? [] + + let (progress, publisher) = Current.shell.downloadWithAria2( + aria2Path, + runtime.url, + destination, + cookies + ) + progressChanged(progress) + return publisher + .map { _ in destination.url } + .eraseToAnyPublisher() + } } diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 2745ec9..55a0ce0 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -105,12 +105,13 @@ class AppState: ObservableObject { // MARK: - Runtimes @Published var downloadableRuntimes: [DownloadableRuntime] = [] - @Published var installedRuntimes: [CoreSimulatorRuntimeInfo] = [] + @Published var installedRuntimes: [CoreSimulatorImage] = [] // MARK: - Publisher Cancellables var cancellables = Set() private var installationPublishers: [Version: AnyCancellable] = [:] + internal var runtimePublishers: [String: AnyCancellable] = [:] private var selectPublisher: AnyCancellable? private var uninstallPublisher: AnyCancellable? private var autoInstallTimer: Timer? @@ -148,6 +149,7 @@ class AppState: ObservableObject { checkIfHelperIsInstalled() setupAutoInstallTimer() setupDefaults() + updateInstalledRuntimes() } func setupDefaults() { @@ -175,7 +177,11 @@ class AppState: ObservableObject { func validateADCSession(path: String) -> AnyPublisher { return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path)) .receive(on: DispatchQueue.main) - .tryMap { _ in + .tryMap { result -> Void in + let httpResponse = result.response as! HTTPURLResponse + if httpResponse.statusCode == 401 { + throw AuthenticationError.notAuthorized + } } .eraseToAnyPublisher() } @@ -796,16 +802,27 @@ class AppState: ObservableObject { func getRunTimes(xcode: Xcode) -> [DownloadableRuntime] { let builds = xcode.sdks?.allBuilds() - - let runtime = builds?.flatMap { sdkBuild in + + let runtimes: [DownloadableRuntime]? = builds?.flatMap { sdkBuild in downloadableRuntimes.filter { - $0.simulatorVersion.buildUpdate == sdkBuild + $0.sdkBuildUpdate == sdkBuild } } - // appState.installedRuntimes has a list of builds that user has installed. - - return runtime ?? [] + + let updatedRuntimes = runtimes?.map { runtime in + var updatedRuntime = runtime + if let coreSimulatorInfo = installedRuntimes.filter({ $0.runtimeInfo.build == runtime.sdkBuildUpdate }).first { + let url = URL(fileURLWithPath: coreSimulatorInfo.path["relative"]!) + updatedRuntime.installState = .installed(Path(url: url)!) + } else { + updatedRuntime.installState = .notInstalled + } + return updatedRuntime + } + + return updatedRuntimes ?? [] } + // MARK: - Private diff --git a/Xcodes/Backend/InstallationStep.swift b/Xcodes/Backend/InstallationStep.swift index ca2001d..9519cf5 100644 --- a/Xcodes/Backend/InstallationStep.swift +++ b/Xcodes/Backend/InstallationStep.swift @@ -1,45 +1,45 @@ -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 } -} +//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/SDKs+Xcode.swift b/Xcodes/Backend/SDKs+Xcode.swift index 9aeca4a..1462b96 100644 --- a/Xcodes/Backend/SDKs+Xcode.swift +++ b/Xcodes/Backend/SDKs+Xcode.swift @@ -26,6 +26,9 @@ extension SDKs { if let watchOS = self.watchOS?.compactMap({ $0.build }) { buildNumbers += watchOS } + if let visionOS = self.visionOS?.compactMap({ $0.build }) { + buildNumbers += visionOS + } return buildNumbers } diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index 0f74a92..a662547 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -221,7 +221,7 @@ struct DownloadRuntimeButton: View { private func install() { guard let runtime = runtime else { return } - // appState.checkMinVersionAndInstall(id: xcode.id) + appState.downloadRuntime(runtime: runtime) } } diff --git a/Xcodes/Backend/XcodeInstallState.swift b/Xcodes/Backend/XcodeInstallState.swift index a02181e..b23b75e 100644 --- a/Xcodes/Backend/XcodeInstallState.swift +++ b/Xcodes/Backend/XcodeInstallState.swift @@ -1,5 +1,6 @@ import Foundation import Path +import XcodesKit enum XcodeInstallState: Equatable { case notInstalled diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index 9b616a6..f18474b 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -257,13 +257,27 @@ struct InfoPane: View { .frame(maxWidth: .infinity, alignment: .leading) ForEach(runtimes, id: \.simulatorVersion.buildUpdate) { runtime in - HStack { - Text("\(runtime.visibleIdentifier)") - .font(.subheadline) - Spacer() - Text(runtime.downloadFileSizeString) - .font(.subheadline) - DownloadRuntimeButton(runtime: runtime) + VStack { + HStack { + Text("\(runtime.visibleIdentifier)") + .font(.subheadline) + Spacer() + Text(runtime.downloadFileSizeString) + .font(.subheadline) + DownloadRuntimeButton(runtime: runtime) + } + switch runtime.installState { + + + case .notInstalled: + Text("NOT INSTALLED") + case .installing(let installationStep): + Text("INSTALLING") + InstallationStepDetailView(installationStep: installationStep) + .fixedSize(horizontal: false, vertical: true) + case .installed(let path): + Text(path.string) + } } } diff --git a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift index 863f204..e10b193 100644 --- a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift +++ b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift @@ -1,4 +1,5 @@ import SwiftUI +import XcodesKit struct InstallationStepDetailView: View { let installationStep: InstallationStep diff --git a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift index e2e81f1..ef716af 100644 --- a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift +++ b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift @@ -1,4 +1,5 @@ import SwiftUI +import XcodesKit struct InstallationStepRowView: View { let installationStep: InstallationStep diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift index fa54998..e33528f 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift @@ -20,7 +20,7 @@ public enum InstallationStep: Equatable, CustomStringConvertible { "(\(stepNumber)/\(stepCount)) \(message)" } - var message: String { + public var message: String { switch self { case .downloading: return localizeString("Downloading") @@ -37,7 +37,7 @@ public enum InstallationStep: Equatable, CustomStringConvertible { } } - var stepNumber: Int { + public var stepNumber: Int { switch self { case .downloading: return 1 case .unarchiving: return 2 @@ -48,7 +48,7 @@ public enum InstallationStep: Equatable, CustomStringConvertible { } } - var stepCount: Int { 6 } + public var stepCount: Int { 6 } } func localizeString(_ key: String, comment: String = "") -> String { if #available(macOS 12, *) { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift index 69dd9da..a85d3c8 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift @@ -13,6 +13,7 @@ public struct CoreSimulatorPlist: Decodable { public struct CoreSimulatorImage: Decodable { public let uuid: String + public let path: [String: String] public let runtimeInfo: CoreSimulatorRuntimeInfo } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift index 0e57b9a..2ab3362 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift @@ -21,9 +21,16 @@ public struct DownloadableRuntime: Codable { public let hostRequirements: HostRequirements? public let name: String public let authentication: Authentication? + public var url: URL { + return URL(string: source)! + } + public var downloadPath: String { + url.path + } // dynamically updated - not decoded public var installState: InstallState = .notInstalled + public var sdkBuildUpdate: String? enum CodingKeys: CodingKey { case category @@ -38,6 +45,7 @@ public struct DownloadableRuntime: Codable { case hostRequirements case name case authentication + case sdkBuildUpdate } var betaNumber: Int? { @@ -108,13 +116,15 @@ extension DownloadableRuntime { case macOS = "com.apple.platform.macosx" case watchOS = "com.apple.platform.watchos" case tvOS = "com.apple.platform.appletvos" - + case visionOS = "com.apple.platform.xros" + var order: Int { switch self { case .iOS: return 1 case .macOS: return 2 case .watchOS: return 3 case .tvOS: return 4 + case .visionOS: return 5 } } @@ -124,6 +134,7 @@ extension DownloadableRuntime { case .macOS: return "macOS" case .watchOS: return "watchOS" case .tvOS: return "tvOS" + case .visionOS: return "visionOS" } } } @@ -156,6 +167,16 @@ extension InstalledRuntime { case tvOS = "com.apple.platform.appletvsimulator" case iOS = "com.apple.platform.iphonesimulator" case watchOS = "com.apple.platform.watchsimulator" + case visionOS = "com.apple.platform.xrsimulator" + + var asPlatformOS: DownloadableRuntime.Platform { + switch self { + case .watchOS: return .watchOS + case .iOS: return .iOS + case .tvOS: return .tvOS + case .visionOS: return .visionOS + } + } } } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift index 429fc48..e269375 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift @@ -38,17 +38,19 @@ public struct RuntimeService { /// Loops through `/Library/Developer/CoreSimulator/images/images.plist` which contains a list of downloaded Simuator Runtimes /// This is different then using `simctl` (`installedRuntimes()`) which only returns the installed runtimes for the selected xcode version. - public func localInstalledRuntimes() async throws -> [CoreSimulatorRuntimeInfo] { + public func localInstalledRuntimes() async throws -> [CoreSimulatorImage] { guard let path = Path("/Library/Developer/CoreSimulator/images/images.plist") else { throw "Could not find images.plist for CoreSimulators" } guard let infoPlistData = FileManager.default.contents(atPath: path.string) else { throw "Could not get data from \(path.string)" } do { let infoPlist: CoreSimulatorPlist = try PropertyListDecoder().decode(CoreSimulatorPlist.self, from: infoPlistData) - return infoPlist.images.map { $0.runtimeInfo } + return infoPlist.images } catch { throw error } } + + }