more runtime download work

This commit is contained in:
Matt Kiazyk 2023-06-23 14:45:13 -05:00
parent 4f25905f4c
commit 7325502853
16 changed files with 269 additions and 77 deletions

View file

@ -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" */ = {

View file

@ -23,8 +23,8 @@
"package": "XcodeReleases",
"repositoryURL": "https://github.com/xcodereleases/data",
"state": {
"branch": null,
"revision": "b47228c688b608e34b3b84079ab6052a24c7a981",
"branch": "main",
"revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc",
"version": null
}
},

View file

@ -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)
}
}
}

View file

@ -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<URL, Error> {
// 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<URL, Error> {
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()
}
}

View file

@ -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<AnyCancellable>()
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<Void, Error> {
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

View file

@ -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 }
//}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -1,5 +1,6 @@
import Foundation
import Path
import XcodesKit
enum XcodeInstallState: Equatable {
case notInstalled

View file

@ -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)
}
}
}

View file

@ -1,4 +1,5 @@
import SwiftUI
import XcodesKit
struct InstallationStepDetailView: View {
let installationStep: InstallationStep

View file

@ -1,4 +1,5 @@
import SwiftUI
import XcodesKit
struct InstallationStepRowView: View {
let installationStep: InstallationStep

View file

@ -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, *) {

View file

@ -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
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}