start moving over to XcodesKit package. Runtimes searching

This commit is contained in:
Matt Kiazyk 2023-06-06 23:29:09 -05:00
parent d8e1069a92
commit 4f25905f4c
31 changed files with 749 additions and 54 deletions

View file

@ -18,7 +18,6 @@
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA25192925A9644800F08414 /* XcodeInstallState.swift */; };
CA378F992466567600A58CE0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA378F982466567600A58CE0 /* AppState.swift */; };
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */; };
CA42DD6E25AEA8B200BC0B0C /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA42DD6D25AEA8B200BC0B0C /* Logger.swift */; };
CA42DD7325AEB04300BC0B0C /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA42DD7225AEB04300BC0B0C /* Logger.swift */; };
CA44901F2463AD34003D8213 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA44901E2463AD34003D8213 /* Tag.swift */; };
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */; };
@ -71,7 +70,6 @@
CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9AC2592EEE900380FEE /* Foundation.swift */; };
CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A62592EEE900380FEE /* Version+Xcode.swift */; };
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B42592EEEA00380FEE /* Process.swift */; };
CABFA9DF2592F07A00380FEE /* Path in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9DE2592F07A00380FEE /* Path */; };
CABFA9E42592F08E00380FEE /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9E32592F08E00380FEE /* Version */; };
CABFA9EE2592F0CC00380FEE /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9ED2592F0CC00380FEE /* SwiftSoup */; };
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9F72592F0F900380FEE /* KeychainAccess */; };
@ -108,6 +106,9 @@
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */; };
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */ = {isa = PBXBuildFile; productRef = E8C0EB19291EF43E0081528A /* XcodesKit */; };
E8C0EB1C291EF9A10081528A /* AppState+Runtimes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C0EB1B291EF9A10081528A /* AppState+Runtimes.swift */; };
E8CBDB8927ADE32300B22292 /* unxip in Copy aria2c */ = {isa = PBXBuildFile; fileRef = E8CBDB8627ADD92000B22292 /* unxip */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
E8CBDB8B27AE02FF00B22292 /* ExperiementsPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */; };
E8D0296F284B029800647641 /* BottomStatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D0296E284B029800647641 /* BottomStatusBar.swift */; };
@ -115,7 +116,9 @@
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
E8F44A1E296B4CD7002D6592 /* Path in Frameworks */ = {isa = PBXBuildFile; productRef = E8F44A1D296B4CD7002D6592 /* Path */; };
E8F81FC4282D8A17006CBD0F /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E8F81FC3282D8A17006CBD0F /* Sparkle */; };
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */ = {isa = PBXBuildFile; productRef = E8FD5726291EE4AC001E004C /* AsyncNetworkService */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -196,7 +199,6 @@
CA25192925A9644800F08414 /* XcodeInstallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeInstallState.swift; sourceTree = "<group>"; };
CA378F982466567600A58CE0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = "<group>"; };
CA42DD6D25AEA8B200BC0B0C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
CA42DD7225AEB04300BC0B0C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = "<group>"; };
@ -297,11 +299,14 @@
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
E2AFDCCA28F024D000864ADD /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = "<group>"; };
E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = "<group>"; };
E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = "<group>"; };
E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = "<group>"; };
E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDKs+Xcode.swift"; sourceTree = "<group>"; };
E8C0EB1B291EF9A10081528A /* AppState+Runtimes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+Runtimes.swift"; sourceTree = "<group>"; };
E8CBDB8627ADD92000B22292 /* unxip */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = unxip; sourceTree = "<group>"; };
E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiementsPreferencePane.swift; sourceTree = "<group>"; };
E8D0296E284B029800647641 /* BottomStatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomStatusBar.swift; sourceTree = "<group>"; };
@ -328,9 +333,11 @@
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */,
CABFA9DF2592F07A00380FEE /* Path in Frameworks */,
CABFA9EE2592F0CC00380FEE /* SwiftSoup in Frameworks */,
E8F44A1E296B4CD7002D6592 /* Path in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -452,6 +459,7 @@
isa = PBXGroup;
children = (
CA378F982466567600A58CE0 /* AppState.swift */,
E8C0EB1B291EF9A10081528A /* AppState+Runtimes.swift */,
CAE424B3259A764700B8B246 /* AppState+Install.swift */,
CABFA9A72592EEE900380FEE /* AppState+Update.swift */,
CAA8589A25A2B83000ACF8C0 /* Aria2CError.swift */,
@ -473,7 +481,6 @@
CAC281D9259F985100B8AB0B /* InstallationStep.swift */,
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */,
CA42DD6D25AEA8B200BC0B0C /* Logger.swift */,
E89342F925EDCC17007CF557 /* NotificationManager.swift */,
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */,
CABFA9AE2592EEE900380FEE /* Path+.swift */,
@ -492,6 +499,7 @@
E81D7E9F2805250100A205FC /* Collection+.swift */,
E8D655BF288DD04700A139C2 /* SelectedActionType.swift */,
E87AB3C42939B65E00D72F43 /* Hardware.swift */,
E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */,
);
path = Backend;
sourceTree = "<group>";
@ -532,6 +540,7 @@
CAD2E7952449574E00113D76 = {
isa = PBXGroup;
children = (
E856BB73291EDD3D00DC438B /* XcodesKit */,
CA8FB5F8256E0F9400469DA5 /* README.md */,
CABFA9D42592EF6300380FEE /* DECISIONS.md */,
CABFA9A02592EAF500380FEE /* R&PLogo.png */,
@ -656,7 +665,6 @@
name = Xcodes;
packageProductDependencies = (
CAA1CB2C255A5262003FD669 /* AppleAPI */,
CABFA9DE2592F07A00380FEE /* Path */,
CABFA9E32592F08E00380FEE /* Version */,
CABFA9ED2592F0CC00380FEE /* SwiftSoup */,
CABFA9F72592F0F900380FEE /* KeychainAccess */,
@ -664,6 +672,9 @@
CA9FF86C25951C6E00E47BAF /* XCModel */,
CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */,
E8F81FC3282D8A17006CBD0F /* Sparkle */,
E8FD5726291EE4AC001E004C /* AsyncNetworkService */,
E8C0EB19291EF43E0081528A /* XcodesKit */,
E8F44A1D296B4CD7002D6592 /* Path */,
);
productName = XcodesMac;
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
@ -737,7 +748,6 @@
);
mainGroup = CAD2E7952449574E00113D76;
packageReferences = (
CABFA9DD2592F07A00380FEE /* XCRemoteSwiftPackageReference "Path" */,
CABFA9E22592F08E00380FEE /* XCRemoteSwiftPackageReference "Version" */,
CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */,
CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */,
@ -746,6 +756,8 @@
CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */,
CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */,
E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */,
E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */,
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
);
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
projectDirPath = "";
@ -854,7 +866,6 @@
36741BFF291E50F500A85AAE /* FileError.swift in Sources */,
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */,
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
CA42DD6E25AEA8B200BC0B0C /* Logger.swift in Sources */,
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
@ -871,6 +882,7 @@
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */,
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
E81D7EA02805250100A205FC /* Collection+.swift in Sources */,
E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */,
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
@ -894,6 +906,7 @@
CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */,
CABFAA432593104F00380FEE /* AboutView.swift in Sources */,
E8D0296F284B029800647641 /* BottomStatusBar.swift in Sources */,
E8C0EB1C291EF9A10081528A /* AppState+Runtimes.swift in Sources */,
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */,
CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */,
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
@ -1416,14 +1429,6 @@
minimumVersion = 0.1.0;
};
};
CABFA9DD2592F07A00380FEE /* XCRemoteSwiftPackageReference "Path" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mxcl/Path.swift";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.16.0;
};
};
CABFA9E22592F08E00380FEE /* XCRemoteSwiftPackageReference "Version" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mxcl/Version";
@ -1464,6 +1469,14 @@
minimumVersion = 0.6.0;
};
};
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mxcl/Path.swift";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
@ -1472,6 +1485,14 @@
minimumVersion = 2.0.0;
};
};
E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/RobotsAndPencils/AsyncHTTPNetworkService";
requirement = {
branch = main;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -1489,11 +1510,6 @@
package = CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */;
productName = ErrorHandling;
};
CABFA9DE2592F07A00380FEE /* Path */ = {
isa = XCSwiftPackageProductDependency;
package = CABFA9DD2592F07A00380FEE /* XCRemoteSwiftPackageReference "Path" */;
productName = Path;
};
CABFA9E32592F08E00380FEE /* Version */ = {
isa = XCSwiftPackageProductDependency;
package = CABFA9E22592F08E00380FEE /* XCRemoteSwiftPackageReference "Version" */;
@ -1519,11 +1535,25 @@
package = CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */;
productName = CombineExpectations;
};
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
isa = XCSwiftPackageProductDependency;
productName = XcodesKit;
};
E8F44A1D296B4CD7002D6592 /* Path */ = {
isa = XCSwiftPackageProductDependency;
package = E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */;
productName = Path;
};
E8F81FC3282D8A17006CBD0F /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
E8FD5726291EE4AC001E004C /* AsyncNetworkService */ = {
isa = XCSwiftPackageProductDependency;
package = E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */;
productName = AsyncNetworkService;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = CAD2E7962449574E00113D76 /* Project object */;

View file

@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "AsyncNetworkService",
"repositoryURL": "https://github.com/RobotsAndPencils/AsyncHTTPNetworkService",
"state": {
"branch": "main",
"revision": "97770856c4e429f880d4b4dd68cfaf286dc00c30",
"version": null
}
},
{
"package": "CombineExpectations",
"repositoryURL": "https://github.com/groue/CombineExpectations",
@ -51,8 +60,8 @@
"repositoryURL": "https://github.com/mxcl/Path.swift",
"state": {
"branch": null,
"revision": "dac007e907a4f4c565cfdc55a9ce148a761a11d5",
"version": "0.16.3"
"revision": "9c6f807b0a76be0e27aecc908bc6f173400d839e",
"version": "1.4.0"
}
},
{

View file

@ -5,6 +5,7 @@ import AppleAPI
import Version
import LegibleError
import os.log
import XcodesKit
/// Downloads and installs Xcodes
extension AppState {

View file

@ -0,0 +1,32 @@
import Foundation
import XcodesKit
import OSLog
extension AppState {
func updateDownloadableRuntimes() {
Task {
do {
let runtimes = try await self.runtimeService.downloadableRuntimes().downloadables
DispatchQueue.main.async {
self.downloadableRuntimes = runtimes
}
try? cacheDownloadableRuntimes(runtimes)
} catch {
Logger.appState.error("Error downloading runtimes: \(error.localizedDescription)")
}
}
}
func updateInstalledRuntimes() {
Task {
do {
let runtimes = try await self.runtimeService.localInstalledRuntimes()
DispatchQueue.main.async {
self.installedRuntimes = runtimes
}
} catch {
Logger.appState.error("Error loading installed runtimes: \(error.localizedDescription)")
}
}
}
}

View file

@ -5,6 +5,7 @@ import Version
import SwiftSoup
import struct XCModel.Xcode
import AppleAPI
import XcodesKit
extension AppState {
@ -36,6 +37,8 @@ extension AppState {
func update() {
guard !isUpdating else { return }
updateDownloadableRuntimes()
updateInstalledRuntimes()
updatePublisher = updateSelectedXcodePath()
.flatMap { _ in
self.updateAvailableXcodes(from: self.dataSource)
@ -125,6 +128,21 @@ extension AppState {
withIntermediateDirectories: true)
try data.write(to: Path.cacheFile.url)
}
// MARK: Runtime Cache
func loadCacheDownloadableRuntimes() throws {
guard let data = Current.files.contents(atPath: Path.runtimeCacheFile.string) else { return }
let runtimes = try JSONDecoder().decode([DownloadableRuntime].self, from: data)
self.downloadableRuntimes = runtimes
}
func cacheDownloadableRuntimes(_ runtimes: [DownloadableRuntime]) throws {
let data = try JSONEncoder().encode(runtimes)
try FileManager.default.createDirectory(at: Path.runtimeCacheFile.url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try data.write(to: Path.runtimeCacheFile.url)
}
}
extension AppState {

View file

@ -7,9 +7,11 @@ import KeychainAccess
import Path
import Version
import os.log
import XcodesKit
class AppState: ObservableObject {
private let client = AppleAPI.Client()
internal let runtimeService = RuntimeService()
// MARK: - Published Properties
@ -99,6 +101,12 @@ class AppState: ObservableObject {
Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption")
}
}
// MARK: - Runtimes
@Published var downloadableRuntimes: [DownloadableRuntime] = []
@Published var installedRuntimes: [CoreSimulatorRuntimeInfo] = []
// MARK: - Publisher Cancellables
var cancellables = Set<AnyCancellable>()
@ -136,6 +144,7 @@ class AppState: ObservableObject {
init() {
guard !isTesting else { return }
try? loadCachedAvailableXcodes()
try? loadCacheDownloadableRuntimes()
checkIfHelperIsInstalled()
setupAutoInstallTimer()
setupDefaults()
@ -783,6 +792,21 @@ class AppState: ObservableObject {
self.allXcodes = newAllXcodes.sorted { $0.version > $1.version }
}
// MARK: Runtimes
func getRunTimes(xcode: Xcode) -> [DownloadableRuntime] {
let builds = xcode.sdks?.allBuilds()
let runtime = builds?.flatMap { sdkBuild in
downloadableRuntimes.filter {
$0.simulatorVersion.buildUpdate == sdkBuild
}
}
// appState.installedRuntimes has a list of builds that user has installed.
return runtime ?? []
}
// MARK: - Private
private func uninstallXcode(path: Path) -> AnyPublisher<Void, Error> {

View file

@ -1,13 +1,13 @@
import Foundation
import Path
extension Entry {
static func isAppBundle(kind: Kind, path: Path) -> Bool {
kind == .directory &&
extension Path {
static func isAppBundle(path: Path) -> Bool {
path.isDirectory &&
path.extension == "app" &&
!path.isSymlink
}
static func infoPlist(kind: Kind, path: Path) -> InfoPlist? {
static func infoPlist(path: Path) -> InfoPlist? {
let infoPlistPath = path.join("Contents").join("Info.plist")
guard
let infoPlistData = try? Data(contentsOf: infoPlistPath.url),
@ -18,10 +18,10 @@ extension Entry {
}
var isAppBundle: Bool {
Entry.isAppBundle(kind: kind, path: path)
Path.isAppBundle(path: self)
}
var infoPlist: InfoPlist? {
Entry.infoPlist(kind: kind, path: path)
Path.infoPlist(path: self)
}
}

View file

@ -3,7 +3,7 @@ import Foundation
import Path
import AppleAPI
import KeychainAccess
import XcodesKit
/**
Lightweight dependency injection using global mutable state :P
@ -166,7 +166,7 @@ public struct Files {
public var installedXcodes = _installedXcodes
public func installedXcode(destination: Path) -> InstalledXcode? {
if Entry.isAppBundle(kind: destination.isDirectory ? .directory : .file, path: destination) && Entry.infoPlist(kind: destination.isDirectory ? .directory : .file, path: destination)?.bundleID == "com.apple.dt.Xcode" {
if Path.isAppBundle(path: destination) && Path.infoPlist(path: destination)?.bundleID == "com.apple.dt.Xcode" {
return InstalledXcode.init(path: destination)
} else {
return nil
@ -175,9 +175,9 @@ public struct Files {
}
private func _installedXcodes(destination: Path) -> [InstalledXcode] {
((try? destination.ls()) ?? [])
destination.ls()
.filter { $0.isAppBundle && $0.infoPlist?.bundleID == "com.apple.dt.Xcode" }
.map { $0.path }
.map { $0 }
.compactMap(InstalledXcode.init)
}

View file

@ -1,10 +0,0 @@
import Foundation
import os.log
extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier!
static let appState = Logger(subsystem: subsystem, category: "appState")
static let helperClient = Logger(subsystem: subsystem, category: "helperClient")
static let subprocess = Logger(subsystem: subsystem, category: "subprocess")
}

View file

@ -28,4 +28,8 @@ extension Path {
}
return path
}
static var runtimeCacheFile: Path {
return xcodesApplicationSupport/"downloadable-runtimes.json"
}
}

View file

@ -2,12 +2,11 @@ import Combine
import Foundation
import os.log
import Path
public typealias ProcessOutput = (status: Int32, out: String, err: String)
import XcodesKit
extension Process {
@discardableResult
static func run(_ executable: Path, 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> {
return run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
}
@ -67,9 +66,3 @@ extension Process {
.eraseToAnyPublisher()
}
}
struct ProcessExecutionError: Error {
let process: Process
let standardOutput: String
let standardError: String
}

View file

@ -0,0 +1,32 @@
//
// SDKs+Xcode.swift
// Xcodes
//
// Created by Matt Kiazyk on 2023-06-05.
// Copyright © 2023 Robots and Pencils. All rights reserved.
//
import Foundation
import struct XCModel.SDKs
extension SDKs {
/// Loops through all SDK's and returns an array of buildNumbers (to be used to correlate runtimes)
func allBuilds() -> [String] {
var buildNumbers: [String] = []
if let iOS = self.iOS?.compactMap({ $0.build }) {
buildNumbers += iOS
}
if let tvOS = self.tvOS?.compactMap({ $0.build }) {
buildNumbers += tvOS
}
if let macOS = self.macOS?.compactMap({ $0.build }) {
buildNumbers += macOS
}
if let watchOS = self.watchOS?.compactMap({ $0.build }) {
buildNumbers += watchOS
}
return buildNumbers
}
}

View file

@ -67,4 +67,5 @@ struct Xcode: Identifiable, CustomStringConvertible {
return nil
}
}
}

View file

@ -1,4 +1,5 @@
import SwiftUI
import XcodesKit
// MARK: - CommandMenu
@ -207,6 +208,23 @@ struct CreateSymbolicLinkButton: View {
}
}
struct DownloadRuntimeButton: View {
@EnvironmentObject var appState: AppState
let runtime: DownloadableRuntime?
var body: some View {
Button(action: install) {
Text("Install")
.help("Install")
}
}
private func install() {
guard let runtime = runtime else { return }
// appState.checkMinVersionAndInstall(id: xcode.id)
}
}
struct CreateSymbolicBetaLinkButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?

View file

@ -54,6 +54,7 @@ struct InfoPane: View {
Divider()
Group{
runtimes(for: xcode)
releaseNotes(for: xcode)
releaseDate(for: xcode)
identicalBuilds(for: xcode)
@ -245,6 +246,29 @@ struct InfoPane: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
@ViewBuilder
private func runtimes(for xcode: Xcode) -> some View {
let runtimes = appState.getRunTimes(xcode: xcode)
VStack(alignment: .leading) {
Text("Platforms")
.font(.headline)
.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)
}
}
}
}
}
struct InfoPane_Previews: PreviewProvider {
@ -329,6 +353,7 @@ struct InfoPane_Previews: PreviewProvider {
),
downloadFileSize: 242342424)
]
})
.previewDisplayName("Populated, Uninstalled")

View file

@ -1,10 +1,37 @@
{\rtf1\ansi\ansicpg1252\cocoartf2639
{\rtf1\ansi\ansicpg1252\cocoartf2709
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
\f0\fs34 \cf0 SwiftSoup\
\f0\fs34 \cf0 AsyncHTTPNetworkService\
\
\fs26 MIT License\
\
Copyright (c) 2022 Robots and Pencils\
\
Permission is hereby granted, free of charge, to any person obtaining a copy\
of this software and associated documentation files (the "Software"), to deal\
in the Software without restriction, including without limitation the rights\
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
copies of the Software, and to permit persons to whom the Software is\
furnished to do so, subject to the following conditions:\
\
The above copyright notice and this permission notice shall be included in all\
copies or substantial portions of the Software.\
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
SOFTWARE.\
\
\
\fs34 SwiftSoup\
\
\fs26 MIT License\

View file

@ -40,6 +40,7 @@
"Compilers" = "Compilers";
"DownloadSize" = "Download Size";
"NoXcodeSelected" = "No Xcode Selected";
"Platforms" = "Platforms";
// Installation Steps
// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"

9
Xcodes/XcodesKit/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View file

@ -0,0 +1,33 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "XcodesKit",
platforms: [.macOS(.v11)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "XcodesKit",
targets: ["XcodesKit"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/RobotsAndPencils/AsyncHTTPNetworkService", branch: "main"),
.package(url: "https://github.com/mxcl/Path.swift", from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "XcodesKit",
dependencies: [
.product(name: "AsyncNetworkService", package: "AsyncHTTPNetworkService"),
.product(name: "Path", package: "Path.swift")
]),
.testTarget(
name: "XcodesKitTests",
dependencies: ["XcodesKit"]),
]
)

View file

@ -0,0 +1,3 @@
# XcodesKit
A description of this package.

View file

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

View file

@ -0,0 +1,12 @@
import Foundation
extension NSRegularExpression {
func firstString(in string: String, options: NSRegularExpression.MatchingOptions = []) -> String? {
let range = NSRange(location: 0, length: string.utf16.count)
guard let firstMatch = firstMatch(in: string, options: options, range: range),
let resultRange = Range(firstMatch.range, in: string) else {
return nil
}
return String(string[resultRange])
}
}

View file

@ -0,0 +1,10 @@
import Foundation
import os.log
extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier!
static public let appState = Logger(subsystem: subsystem, category: "appState")
static public let helperClient = Logger(subsystem: subsystem, category: "helperClient")
static public let subprocess = Logger(subsystem: subsystem, category: "subprocess")
}

View file

@ -0,0 +1,34 @@
//
// InstallState.swift
//
//
// Created by Matt Kiazyk on 2023-06-06.
//
import Foundation
import Path
public enum InstallState: Equatable {
case notInstalled
case installing(InstallationStep)
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,60 @@
//
// InstallationStep.swift
//
//
// Created by Matt Kiazyk on 2023-06-06.
//
import Foundation
// A numbered step
public enum InstallationStep: 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)"
}
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 }
}
func localizeString(_ key: String, comment: String = "") -> String {
if #available(macOS 12, *) {
return String(localized: String.LocalizationValue(key))
} else {
return NSLocalizedString(key, comment: comment)
}
}

View file

@ -0,0 +1,21 @@
//
// CoreSimulatorImage.swift
//
//
// Created by Matt Kiazyk on 2023-01-08.
//
import Foundation
public struct CoreSimulatorPlist: Decodable {
public let images: [CoreSimulatorImage]
}
public struct CoreSimulatorImage: Decodable {
public let uuid: String
public let runtimeInfo: CoreSimulatorRuntimeInfo
}
public struct CoreSimulatorRuntimeInfo: Decodable {
public let build: String
}

View file

@ -0,0 +1,161 @@
import Foundation
public struct DownloadableRuntimesResponse: Codable {
public let sdkToSimulatorMappings: [SDKToSimulatorMapping]
public let sdkToSeedMappings: [SDKToSeedMapping]
public let refreshInterval: Int
public let downloadables: [DownloadableRuntime]
public let version: String
}
public struct DownloadableRuntime: Codable {
public let category: Category
public let simulatorVersion: SimulatorVersion
public let source: String
public let dictionaryVersion: Int
public let contentType: ContentType
public let platform: Platform
public let identifier: String
public let version: String
public let fileSize: Int
public let hostRequirements: HostRequirements?
public let name: String
public let authentication: Authentication?
// dynamically updated - not decoded
public var installState: InstallState = .notInstalled
enum CodingKeys: CodingKey {
case category
case simulatorVersion
case source
case dictionaryVersion
case contentType
case platform
case identifier
case version
case fileSize
case hostRequirements
case name
case authentication
}
var betaNumber: Int? {
enum Regex { static let shared = try! NSRegularExpression(pattern: "b[0-9]+$") }
guard var foundString = Regex.shared.firstString(in: identifier) else { return nil }
foundString.removeFirst()
return Int(foundString)!
}
var completeVersion: String {
makeVersion(for: simulatorVersion.version, betaNumber: betaNumber)
}
public var visibleIdentifier: String {
return platform.shortName + " " + completeVersion
}
func makeVersion(for osVersion: String, betaNumber: Int?) -> String {
let betaSuffix = betaNumber.flatMap { "-beta\($0)" } ?? ""
return osVersion + betaSuffix
}
public var downloadFileSizeString: String {
return ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file)
}
}
public struct SDKToSeedMapping: Codable {
public let buildUpdate: String
public let platform: DownloadableRuntime.Platform
public let seedNumber: Int
}
public struct SDKToSimulatorMapping: Codable {
public let sdkBuildUpdate: String
public let simulatorBuildUpdate: String
public let sdkIdentifier: String
}
extension DownloadableRuntime {
public struct SimulatorVersion: Codable {
public let buildUpdate: String
public let version: String
}
public struct HostRequirements: Codable {
let maxHostVersion: String?
let excludedHostArchitectures: [String]?
let minHostVersion: String?
let minXcodeVersion: String?
}
public enum Authentication: String, Codable {
case virtual = "virtual"
}
public enum Category: String, Codable {
case simulator = "simulator"
}
public enum ContentType: String, Codable {
case diskImage = "diskImage"
case package = "package"
}
public enum Platform: String, Codable {
case iOS = "com.apple.platform.iphoneos"
case macOS = "com.apple.platform.macosx"
case watchOS = "com.apple.platform.watchos"
case tvOS = "com.apple.platform.appletvos"
var order: Int {
switch self {
case .iOS: return 1
case .macOS: return 2
case .watchOS: return 3
case .tvOS: return 4
}
}
var shortName: String {
switch self {
case .iOS: return "iOS"
case .macOS: return "macOS"
case .watchOS: return "watchOS"
case .tvOS: return "tvOS"
}
}
}
}
public struct InstalledRuntime: Decodable {
let build: String
let deletable: Bool
let identifier: UUID
let kind: Kind
let lastUsedAt: Date?
let path: String
let platformIdentifier: Platform
let runtimeBundlePath: String
let runtimeIdentifier: String
let signatureState: String
let state: String
let version: String
let sizeBytes: Int?
}
extension InstalledRuntime {
enum Kind: String, Decodable {
case diskImage = "Disk Image"
case bundled = "Bundled with Xcode"
case legacyDownload = "Legacy Download"
}
enum Platform: String, Decodable {
case tvOS = "com.apple.platform.appletvsimulator"
case iOS = "com.apple.platform.iphonesimulator"
case watchOS = "com.apple.platform.watchsimulator"
}
}

View file

@ -0,0 +1,55 @@
import Foundation
import AsyncNetworkService
import Path
extension URL {
static let downloadableRuntimes = URL(string: "https://devimages-cdn.apple.com/downloads/xcode/simulators/index2.dvtdownloadableindex")!
}
public struct RuntimeService {
var networkService: AsyncHTTPNetworkService
public init() {
networkService = AsyncHTTPNetworkService()
}
public func downloadableRuntimes() async throws -> DownloadableRuntimesResponse {
let urlRequest = URLRequest(url: .downloadableRuntimes)
// Apple gives a plist for download
let (data, _) = try await networkService.requestData(urlRequest, validators: [])
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
return decodedResponse
}
public func installedRuntimes() async throws -> [InstalledRuntime] {
// This only uses the Selected Xcode, so we don't know what other SDK's have been installed in previous versions
let output = try await Current.shell.installedRuntimes()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let outputDictionary = try decoder.decode([String: InstalledRuntime].self, from: output.out.data(using: .utf8)!)
return outputDictionary.values.sorted { first, second in
return first.identifier.uuidString.compare(second.identifier.uuidString, options: .numeric) == .orderedAscending
}
}
/// 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] {
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 }
} catch {
throw error
}
}
}
extension String: Error {}

View file

@ -0,0 +1,66 @@
import Foundation
import Path
import os.log
public typealias ProcessOutput = (status: Int32, out: String, err: String)
extension Process {
static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) async throws -> ProcessOutput {
return try await run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
}
static func run(_ executable: URL, workingDirectory: URL? = nil, input: String? = nil, _ arguments: [String]) async throws -> ProcessOutput {
let process = Process()
process.currentDirectoryURL = workingDirectory ?? executable.deletingLastPathComponent()
process.executableURL = executable
process.arguments = arguments
let (stdout, stderr) = (Pipe(), Pipe())
process.standardOutput = stdout
process.standardError = stderr
if let input = input {
let inputPipe = Pipe()
process.standardInput = inputPipe.fileHandleForReading
inputPipe.fileHandleForWriting.write(Data(input.utf8))
inputPipe.fileHandleForWriting.closeFile()
}
do {
Logger.subprocess.info("Process.run executable: \(executable), input: \(input ?? ""), arguments: \(arguments.joined(separator: ", "))")
try process.run()
process.waitUntilExit()
let output = String(data: stdout.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let error = String(data: stderr.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
Logger.subprocess.info("Process.run output: \(output)")
if !error.isEmpty {
Logger.subprocess.error("Process.run error: \(error)")
}
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
throw ProcessExecutionError(process: process, standardOutput: output, standardError: error)
}
return (process.terminationStatus, output, error)
} catch {
throw error
}
}
}
public struct ProcessExecutionError: Error {
public let process: Process
public let standardOutput: String
public let standardError: String
public init(process: Process, standardOutput: String, standardError: String) {
self.process = process
self.standardOutput = standardOutput
self.standardError = standardError
}
}

View file

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

View file

@ -0,0 +1,11 @@
import XCTest
@testable import XcodesKit
final class XcodesKitTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(XcodesKit().text, "Hello, World!")
}
}