From 4f25905f4c5544f8089e33dbd794f48e604030cf Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Tue, 6 Jun 2023 23:29:09 -0500 Subject: [PATCH 1/7] start moving over to XcodesKit package. Runtimes searching --- Xcodes.xcodeproj/project.pbxproj | 72 +++++--- .../xcshareddata/swiftpm/Package.resolved | 13 +- Xcodes/Backend/AppState+Install.swift | 1 + Xcodes/Backend/AppState+Runtimes.swift | 32 ++++ Xcodes/Backend/AppState+Update.swift | 18 ++ Xcodes/Backend/AppState.swift | 24 +++ Xcodes/Backend/Entry+.swift | 12 +- Xcodes/Backend/Environment.swift | 8 +- Xcodes/Backend/Logger.swift | 10 -- Xcodes/Backend/Path+.swift | 4 + Xcodes/Backend/Process.swift | 11 +- Xcodes/Backend/SDKs+Xcode.swift | 32 ++++ Xcodes/Backend/Xcode.swift | 1 + Xcodes/Backend/XcodeCommands.swift | 18 ++ Xcodes/Frontend/InfoPane/InfoPane.swift | 25 +++ Xcodes/Resources/Licenses.rtf | 31 +++- Xcodes/Resources/en.lproj/Localizable.strings | 1 + Xcodes/XcodesKit/.gitignore | 9 + Xcodes/XcodesKit/Package.swift | 33 ++++ Xcodes/XcodesKit/README.md | 3 + .../Sources/XcodesKit/Environment.swift | 7 + .../XcodesKit/Extensions/Foundation.swift | 12 ++ .../Sources/XcodesKit/Extensions/Logger.swift | 10 ++ .../XcodesKit/Models/InstallState.swift | 34 ++++ .../XcodesKit/Models/InstallationStep.swift | 60 +++++++ .../Models/Runtimes/CoreSimulatorImage.swift | 21 +++ .../XcodesKit/Models/Runtimes/Runtimes.swift | 161 ++++++++++++++++++ .../XcodesKit/Services/RuntimeService.swift | 55 ++++++ .../Sources/XcodesKit/Shell/Process.swift | 66 +++++++ .../Sources/XcodesKit/Shell/Shell.swift | 8 + .../Tests/XcodesKitTests/XcodesKitTests.swift | 11 ++ 31 files changed, 749 insertions(+), 54 deletions(-) create mode 100644 Xcodes/Backend/AppState+Runtimes.swift delete mode 100644 Xcodes/Backend/Logger.swift create mode 100644 Xcodes/Backend/SDKs+Xcode.swift create mode 100644 Xcodes/XcodesKit/.gitignore create mode 100644 Xcodes/XcodesKit/Package.swift create mode 100644 Xcodes/XcodesKit/README.md create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Foundation.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Logger.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallState.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift create mode 100644 Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 3f9e7d7..56d06e7 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -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 = ""; }; CA378F982466567600A58CE0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = ""; }; - CA42DD6D25AEA8B200BC0B0C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; CA42DD7225AEB04300BC0B0C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = ""; }; @@ -297,11 +299,14 @@ CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = ""; }; E2AFDCCA28F024D000864ADD /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = ""; }; + E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = ""; }; E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = ""; }; E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = ""; }; E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; + E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDKs+Xcode.swift"; sourceTree = ""; }; + E8C0EB1B291EF9A10081528A /* AppState+Runtimes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+Runtimes.swift"; sourceTree = ""; }; E8CBDB8627ADD92000B22292 /* unxip */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = unxip; sourceTree = ""; }; E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiementsPreferencePane.swift; sourceTree = ""; }; E8D0296E284B029800647641 /* BottomStatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomStatusBar.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */; diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 58f4e08..abfd4fe 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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" } }, { diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index ccf637d..a889e69 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -5,6 +5,7 @@ import AppleAPI import Version import LegibleError import os.log +import XcodesKit /// Downloads and installs Xcodes extension AppState { diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift new file mode 100644 index 0000000..a07f798 --- /dev/null +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -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)") + } + } + } +} diff --git a/Xcodes/Backend/AppState+Update.swift b/Xcodes/Backend/AppState+Update.swift index 8dad172..90f531f 100644 --- a/Xcodes/Backend/AppState+Update.swift +++ b/Xcodes/Backend/AppState+Update.swift @@ -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 { diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index ebd44b5..2745ec9 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -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() @@ -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 { diff --git a/Xcodes/Backend/Entry+.swift b/Xcodes/Backend/Entry+.swift index c6e8c11..b195fb0 100644 --- a/Xcodes/Backend/Entry+.swift +++ b/Xcodes/Backend/Entry+.swift @@ -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) } } diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index 9a43ec8..2c93381 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -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) } diff --git a/Xcodes/Backend/Logger.swift b/Xcodes/Backend/Logger.swift deleted file mode 100644 index 57540b8..0000000 --- a/Xcodes/Backend/Logger.swift +++ /dev/null @@ -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") -} diff --git a/Xcodes/Backend/Path+.swift b/Xcodes/Backend/Path+.swift index 36e0042..8bcc59c 100644 --- a/Xcodes/Backend/Path+.swift +++ b/Xcodes/Backend/Path+.swift @@ -28,4 +28,8 @@ extension Path { } return path } + + static var runtimeCacheFile: Path { + return xcodesApplicationSupport/"downloadable-runtimes.json" + } } diff --git a/Xcodes/Backend/Process.swift b/Xcodes/Backend/Process.swift index fe96001..77935df 100644 --- a/Xcodes/Backend/Process.swift +++ b/Xcodes/Backend/Process.swift @@ -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 { + static func run(_ executable: any Pathish, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher { 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 -} diff --git a/Xcodes/Backend/SDKs+Xcode.swift b/Xcodes/Backend/SDKs+Xcode.swift new file mode 100644 index 0000000..9aeca4a --- /dev/null +++ b/Xcodes/Backend/SDKs+Xcode.swift @@ -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 + } +} diff --git a/Xcodes/Backend/Xcode.swift b/Xcodes/Backend/Xcode.swift index 603e35d..4003ac2 100644 --- a/Xcodes/Backend/Xcode.swift +++ b/Xcodes/Backend/Xcode.swift @@ -67,4 +67,5 @@ struct Xcode: Identifiable, CustomStringConvertible { return nil } } + } diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index 76e9924..0f74a92 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -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? diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index 88195da..9b616a6 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -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") diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index 93bc0ff..9d4ab70 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -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\ diff --git a/Xcodes/Resources/en.lproj/Localizable.strings b/Xcodes/Resources/en.lproj/Localizable.strings index 59a8676..2ae234e 100644 --- a/Xcodes/Resources/en.lproj/Localizable.strings +++ b/Xcodes/Resources/en.lproj/Localizable.strings @@ -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" diff --git a/Xcodes/XcodesKit/.gitignore b/Xcodes/XcodesKit/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Xcodes/XcodesKit/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Xcodes/XcodesKit/Package.swift b/Xcodes/XcodesKit/Package.swift new file mode 100644 index 0000000..81447ab --- /dev/null +++ b/Xcodes/XcodesKit/Package.swift @@ -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"]), + ] +) diff --git a/Xcodes/XcodesKit/README.md b/Xcodes/XcodesKit/README.md new file mode 100644 index 0000000..5312c49 --- /dev/null +++ b/Xcodes/XcodesKit/README.md @@ -0,0 +1,3 @@ +# XcodesKit + +A description of this package. diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift new file mode 100644 index 0000000..a988ce9 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct Environment { + public var shell = Shell() +} + +public var Current = Environment() diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Foundation.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Foundation.swift new file mode 100644 index 0000000..02b1e02 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Foundation.swift @@ -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]) + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Logger.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Logger.swift new file mode 100644 index 0000000..222e908 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Logger.swift @@ -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") +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallState.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallState.swift new file mode 100644 index 0000000..8aa6b1e --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallState.swift @@ -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 + } + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift new file mode 100644 index 0000000..fa54998 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/InstallationStep.swift @@ -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) + } + +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift new file mode 100644 index 0000000..69dd9da --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift @@ -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 +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift new file mode 100644 index 0000000..0e57b9a --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift @@ -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" + } +} + diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift new file mode 100644 index 0000000..429fc48 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift @@ -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 {} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift new file mode 100644 index 0000000..b83a079 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift @@ -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 + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift new file mode 100644 index 0000000..520ad0a --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift @@ -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") + } +} diff --git a/Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift b/Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift new file mode 100644 index 0000000..98f0c96 --- /dev/null +++ b/Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift @@ -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!") + } +} From 73255028533299af13d0702c09e73a9d78d7ed20 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Fri, 23 Jun 2023 14:45:13 -0500 Subject: [PATCH 2/7] more runtime download work --- Xcodes.xcodeproj/project.pbxproj | 14 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- Xcodes/Backend/AppState+Install.swift | 9 ++ Xcodes/Backend/AppState+Runtimes.swift | 124 +++++++++++++++++- Xcodes/Backend/AppState.swift | 33 +++-- Xcodes/Backend/InstallationStep.swift | 90 ++++++------- Xcodes/Backend/SDKs+Xcode.swift | 3 + Xcodes/Backend/XcodeCommands.swift | 2 +- Xcodes/Backend/XcodeInstallState.swift | 1 + Xcodes/Frontend/InfoPane/InfoPane.swift | 28 +++- .../InfoPane/InstallationStepDetailView.swift | 1 + .../XcodeList/InstallationStepRowView.swift | 1 + .../XcodesKit/Models/InstallationStep.swift | 6 +- .../Models/Runtimes/CoreSimulatorImage.swift | 1 + .../XcodesKit/Models/Runtimes/Runtimes.swift | 23 +++- .../XcodesKit/Services/RuntimeService.swift | 6 +- 16 files changed, 269 insertions(+), 77 deletions(-) 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 } } + + } From dc5a8b03b6a30706be4eb9866ca5277eaff6fbf5 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Fri, 22 Sep 2023 15:35:26 -0500 Subject: [PATCH 3/7] WIP download runtime, refactor --- Xcodes/Backend/AppState+Install.swift | 3 +- Xcodes/Backend/AppState+Runtimes.swift | 127 ++++++++++++++---- Xcodes/Backend/AppState.swift | 39 +++--- Xcodes/Backend/Environment.swift | 102 +++++++++++++- Xcodes/Backend/Path+.swift | 11 ++ Xcodes/Frontend/InfoPane/InfoPane.swift | 31 +++-- .../XcodesKit/Services/RuntimeService.swift | 27 ++++ .../Sources/XcodesKit/Shell/Shell.swift | 18 +++ 8 files changed, 297 insertions(+), 61 deletions(-) diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 70f82d0..41d71cc 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -466,7 +466,8 @@ 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) { + } + func setInstallationStep(of runtime: DownloadableRuntime, to step: InstallationStep) { DispatchQueue.main.async { guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return } diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index 2f1c568..092accc 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -46,28 +46,38 @@ 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 } - ) + Task { + try? await downloadRunTimeFull(runtime: runtime) + } + +// 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 { +// Logger.appState.error("Error downloading runtime: \(error.localizedDescription)") +//// // 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 + func downloadRunTimeFull(runtime: DownloadableRuntime) async throws { + // sets a proper cookie for runtimes + try await validateADCSession(path: runtime.downloadPath) + + let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2 + Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)") + return validateADCSession(path: runtime.downloadPath) .flatMap { _ in @@ -82,6 +92,18 @@ extension AppState { }) .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() } @@ -92,7 +114,9 @@ extension AppState { // 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: "."))" +// let expectedRuntimePath = Path.xcodesApplicationSupport/"\(runtime.name).\(runtime.name.suffix(fromLast: "."))" + let url = URL(string: runtime.source)! + let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)" // 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 @@ -106,7 +130,8 @@ extension AppState { .eraseToAnyPublisher() } else { -// let destination = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).\(availableXcode.filename.suffix(fromLast: "."))" + + Logger.appState.info("Downloading runtime: \(url.lastPathComponent)") switch downloader { case .aria2: let aria2Path = Path(url: Bundle.main.url(forAuxiliaryExecutable: "aria2c")!)! @@ -115,12 +140,7 @@ extension AppState { to: expectedRuntimePath, aria2Path: aria2Path, progressChanged: progressChanged) -// return downloadXcodeWithAria2( -// availableXcode, -// to: destination, -// aria2Path: aria2Path, -// progressChanged: progressChanged -// ) + case .urlSession: return Just(runtime.url) @@ -134,7 +154,6 @@ extension AppState { // ) } } - } public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher { @@ -151,4 +170,54 @@ extension AppState { .map { _ in destination.url } .eraseToAnyPublisher() } + + public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path, progressChanged: @escaping (Progress) -> Void) async -> URL { + + } + + 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 { + 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() + } + } diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 55a0ce0..2fb00b8 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -186,6 +186,14 @@ class AppState: ObservableObject { .eraseToAnyPublisher() } + func validateADCSession(path: String) async throws { + let result = try await Current.network.dataTaskAsync(with: URLRequest.downloadADCAuth(path: path)) + let httpResponse = result.1 as! HTTPURLResponse + if httpResponse.statusCode == 401 { + throw AuthenticationError.notAuthorized + } + } + func validateSession() -> AnyPublisher { return Current.network.validateSession() @@ -799,30 +807,17 @@ class AppState: ObservableObject { } // MARK: Runtimes - func getRunTimes(xcode: Xcode) -> [DownloadableRuntime] { - - let builds = xcode.sdks?.allBuilds() - - let runtimes: [DownloadableRuntime]? = builds?.flatMap { sdkBuild in - downloadableRuntimes.filter { - $0.sdkBuildUpdate == sdkBuild - } + func runtimeInstallPath(xcode: Xcode, runtime: DownloadableRuntime) -> Path? { + if let coreSimulatorInfo = installedRuntimes.filter({ $0.runtimeInfo.build == runtime.sdkBuildUpdate }).first { + let urlString = coreSimulatorInfo.path["relative"]! + // app was not allowed to open up file:// url's so remove + let fileRemovedString = urlString.replacingOccurrences(of: "file://", with: "") + let url = URL(fileURLWithPath: fileRemovedString) + + return Path(url: url)! } - - 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 ?? [] + return nil } - // MARK: - Private diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index 2c93381..8b7da37 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -112,6 +112,101 @@ 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 { +// 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)) +// } else { +// return promise(.failure(ProcessExecutionError(process: process, standardOutput: "", standardError: ""))) +// } +// } +// promise(.success(())) +// } +// } +// } +// .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")!)! return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"]) @@ -189,10 +284,15 @@ public struct Network { .mapError { $0 as Error } .eraseToAnyPublisher() } + public func dataTask(with request: URLRequest) -> AnyPublisher { dataTask(request) } - + + public func dataTaskAsync(with request: URLRequest) async throws -> (Data, URLResponse) { + return try await AppleAPI.Current.network.session.data(for: request) + } + public var downloadTask: (URL, URL, Data?) -> (Progress, AnyPublisher<(saveLocation: URL, response: URLResponse), Error>) = { AppleAPI.Current.network.session.downloadTask(with: $0, to: $1, resumingWith: $2) } public func downloadTask(with url: URL, to saveLocation: URL, resumingWith resumeData: Data?) -> (progress: Progress, publisher: AnyPublisher<(saveLocation: URL, response: URLResponse), Error>) { diff --git a/Xcodes/Backend/Path+.swift b/Xcodes/Backend/Path+.swift index 8bcc59c..06bbe63 100644 --- a/Xcodes/Backend/Path+.swift +++ b/Xcodes/Backend/Path+.swift @@ -32,4 +32,15 @@ extension Path { static var runtimeCacheFile: Path { return xcodesApplicationSupport/"downloadable-runtimes.json" } + + static var xcodesCaches: Path { + return caches/"com.xcodesorg.xcodesapp" + } + + @discardableResult + func setCurrentUserAsOwner() -> Path { + let user = ProcessInfo.processInfo.environment["SUDO_USER"] ?? NSUserName() + try? FileManager.default.setAttributes([.ownerAccountName: user], ofItemAtPath: string) + return self + } } diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index f18474b..7896426 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -249,14 +249,22 @@ struct InfoPane: View { @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 + let builds = xcode.sdks?.allBuilds() + let runtimes = builds?.flatMap { sdkBuild in + appState.downloadableRuntimes.filter { + $0.sdkBuildUpdate == sdkBuild + } + } +// let runtimes = appState.getRunTimes(xcode: xcode) + + ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in VStack { HStack { Text("\(runtime.visibleIdentifier)") @@ -264,19 +272,26 @@ struct InfoPane: View { Spacer() Text(runtime.downloadFileSizeString) .font(.subheadline) - DownloadRuntimeButton(runtime: runtime) + + // it's installed if we have a path + if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) { + Button(action: { appState.reveal(path: path.string) }) { + Image(systemName: "arrow.right.circle.fill") + } + .buttonStyle(PlainButtonStyle()) + .help("RevealInFinder") + } else { + 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) + default: + EmptyView() } } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift index e269375..c767207 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift @@ -8,6 +8,10 @@ extension URL { public struct RuntimeService { var networkService: AsyncHTTPNetworkService + public enum Error: LocalizedError, Equatable { + case unavailableRuntime(String) + case failedMountingDMG + } public init() { networkService = AsyncHTTPNetworkService() @@ -50,7 +54,30 @@ public struct RuntimeService { } } + public func installRuntimeImage(dmgURL: URL) throws { + Task { + _ = try await Current.shell.installRuntimeImage(dmgURL) + } + } + public func mountDMG(dmgUrl: URL) async throws -> URL { + let resultPlist = try await Current.shell.mountDmg(dmgUrl) + + let dict = try? (PropertyListSerialization.propertyList(from: resultPlist.out.data(using: .utf8)!, format: nil) as? NSDictionary) + let systemEntities = dict?["system-entities"] as? NSArray + guard let path = systemEntities?.compactMap ({ ($0 as? NSDictionary)?["mount-point"] as? String }).first else { + throw Error.failedMountingDMG + } + return URL(fileURLWithPath: path) + } + + public func unmountDMG(mountedURL: URL) async throws { + let url = try await Current.shell.unmountDmg(mountedURL) + } + + public func expand(pkgPath: Path, expandedPkgPath: Path) async throws { + _ = try await Current.shell.expandPkg(pkgPath.url, expandedPkgPath.url) + } } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift index 520ad0a..81157d7 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Shell.swift @@ -5,4 +5,22 @@ public struct Shell { public var installedRuntimes: () async throws -> ProcessOutput = { try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "list", "-j") } + public var mountDmg: (URL) async throws -> ProcessOutput = { + try await Process.run(Path.root.usr.bin.join("hdiutil"), "attach", "-nobrowse", "-plist", $0.path) + } + public var unmountDmg: (URL) async throws -> ProcessOutput = { + try await Process.run(Path.root.usr.bin.join("hdiutil"), "detach", $0.path) + } + public var expandPkg: (URL, URL) async throws -> ProcessOutput = { + try await Process.run(Path.root.usr.sbin.join("pkgutil"), "--verbose", "--expand", $0.path, $1.path) + } + public var createPkg: (URL, URL) async throws -> ProcessOutput = { + try await Process.run(Path.root.usr.sbin.join("pkgutil"), "--flatten", $0.path, $1.path) + } + public var installPkg: (URL, String) async throws -> ProcessOutput = { + try await Process.run(Path.root.usr.sbin.join("installer"), "-pkg", $0.path, "-target", $1) + } + public var installRuntimeImage: (URL) async throws -> ProcessOutput = { + try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "add", $0.path) + } } From 6ffce23616a055889b526c676333eade182d89e2 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Thu, 23 Nov 2023 10:37:41 -0600 Subject: [PATCH 4/7] 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, From ec4dc2b3d0bc1698126b5b528983854d206cf751 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Thu, 23 Nov 2023 13:30:59 -0600 Subject: [PATCH 5/7] runtime clean up --- Xcodes.xcodeproj/project.pbxproj | 4 ++ Xcodes/Backend/AppState+Install.swift | 2 +- Xcodes/Backend/AppState+Runtimes.swift | 58 ++++++++----------- Xcodes/Backend/Environment.swift | 6 ++ Xcodes/Frontend/InfoPane/InfoPane.swift | 3 +- .../RuntimeInstallationStepDetailView.swift | 53 +++++++++++++++++ .../Models/Runtimes/RuntimeInstallState.swift | 2 +- .../Runtimes/RuntimeInstallationStep.swift | 24 ++------ .../XcodesKit/Services/RuntimeService.swift | 17 ++++-- 9 files changed, 107 insertions(+), 62 deletions(-) create mode 100644 Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 17f8687..a6ac845 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; }; E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; }; E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; }; + E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; }; E872EE4E2808D4F100D3DD8B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E872EE502808D4F100D3DD8B /* Localizable.strings */; }; E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; }; E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; }; @@ -300,6 +301,7 @@ CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = ""; }; E2AFDCCA28F024D000864ADD /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = ""; }; + E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = ""; }; E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = ""; }; E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = ""; }; @@ -623,6 +625,7 @@ children = ( CAFBDC67259A308B003DCC5A /* InfoPane.swift */, E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */, + E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */, ); path = InfoPane; sourceTree = ""; @@ -875,6 +878,7 @@ CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */, CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */, CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */, + E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */, CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */, 36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */, diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 936bb6e..ae562d3 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -499,9 +499,9 @@ extension AppState { Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal) } } + 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) diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index f8b6489..4c19820 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -49,9 +49,18 @@ extension AppState { Task { do { try await downloadRunTimeFull(runtime: runtime) + + DispatchQueue.main.async { + guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return } + self.downloadableRuntimes[index].installState = .installed + } } catch { Logger.appState.error("Error downloading runtime: \(error.localizedDescription)") + DispatchQueue.main.async { + self.error = error + self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription) + } } } @@ -83,6 +92,7 @@ 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)) @@ -90,15 +100,23 @@ extension AppState { }).async() Logger.appState.debug("Done downloading: \(url)") - //self.setInstallationStep(of: runtime, to: .downloading(progress: progress)) + DispatchQueue.main.async { + self.setInstallationStep(of: runtime, to: .installing) + } switch runtime.contentType { case .package: - try await self.installFromPackage(dmgURL: url, runtime: runtime) + // not supported yet (do we need to for old packages?) + throw "Installing via package not support - please install manually from \(url.description)" case .diskImage: try await self.installFromImage(dmgURL: url) + DispatchQueue.main.async { + self.setInstallationStep(of: runtime, to: .trashingArchive) + } + try Current.files.removeItem(at: url) } } + @MainActor 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 @@ -139,9 +157,9 @@ extension AppState { .setFailureType(to: Error.self) .eraseToAnyPublisher() -// return downloadXcodeWithURLSession( -// availableXcode, -// to: destination, +// return downloadRuntimeWithURLSession( +// runtime, +// to: expectedRuntimePath, // progressChanged: progressChanged // ) } @@ -163,36 +181,8 @@ extension AppState { .eraseToAnyPublisher() } - public func installFromImage(dmgURL: URL) async throws { - - try? self.runtimeService.installRuntimeImage(dmgURL: dmgURL) - - } - - public func installFromPackage(dmgURL: URL, runtime: DownloadableRuntime) async throws { - Logger.appState.info("Mounting DMG") - - 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)") - } + try await self.runtimeService.installRuntimeImage(dmgURL: dmgURL) } } diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index d84ad26..3784891 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -240,6 +240,12 @@ public struct Files { return nil } } + + public var write: (Data, URL) throws -> Void = { try $0.write(to: $1) } + + public func write(_ data: Data, to url: URL) throws { + try write(data, url) + } } private func _installedXcodes(destination: Path) -> [InstalledXcode] { diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index 625a5ae..399718d 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -288,8 +288,7 @@ struct InfoPane: View { switch runtime.installState { case .installing(let installationStep): - Text("INSTALLING") - InstallationStepDetailView(installationStep: installationStep) + RuntimeInstallationStepDetailView(installationStep: installationStep) .fixedSize(horizontal: false, vertical: true) default: EmptyView() diff --git a/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift new file mode 100644 index 0000000..f59f041 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift @@ -0,0 +1,53 @@ +// +// RuntimeInstallationStepDetailView.swift +// Xcodes +// +// Created by Matt Kiazyk on 2023-11-23. +// Copyright © 2023 Robots and Pencils. All rights reserved. +// + +import SwiftUI +import XcodesKit + +struct RuntimeInstallationStepDetailView: View { + let installationStep: RuntimeInstallationStep + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(String(format: localizeString("InstallationStepDescription"), installationStep.stepNumber, installationStep.stepCount, installationStep.message)) + + switch installationStep { + case let .downloading(progress): + ObservingProgressIndicator( + progress, + controlSize: .regular, + style: .bar, + showsAdditionalDescription: true + ) + + case .installing, .trashingArchive: + ProgressView() + .scaleEffect(0.5) + } + } + } +} + +#Preview("Downloading") { + RuntimeInstallationStepDetailView( + installationStep: .downloading( + progress: configure(Progress()) { + $0.kind = .file + $0.fileOperationKind = .downloading + $0.estimatedTimeRemaining = 123 + $0.totalUnitCount = 11944848484 + $0.completedUnitCount = 848444920 + $0.throughput = 9211681 + } + )) +} +#Preview("Installing") { + RuntimeInstallationStepDetailView( + installationStep: .installing + ) +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift index 25bc5bc..84c4c8d 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift @@ -11,7 +11,7 @@ import Path public enum RuntimeInstallState: Equatable { case notInstalled case installing(RuntimeInstallationStep) - case installed(Path) + case installed var notInstalled: Bool { switch self { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift index 21946a1..27b4e17 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift @@ -9,11 +9,8 @@ import Foundation public enum RuntimeInstallationStep: Equatable, CustomStringConvertible { case downloading(progress: Progress) - case unarchiving - case moving(destination: String) + case installing case trashingArchive - case checkingSecurity - case finishing public var description: String { "(\(stepNumber)/\(stepCount)) \(message)" @@ -23,29 +20,20 @@ public enum RuntimeInstallationStep: Equatable, CustomStringConvertible { switch self { case .downloading: return localizeString("Downloading") - case .unarchiving: - return localizeString("Unarchiving") - case .moving(let destination): - return String(format: localizeString("Moving"), destination) + case .installing: + return localizeString("Installing") 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 + case .installing: return 2 + case .trashingArchive: return 3 } } - public var stepCount: Int { 6 } + public var stepCount: Int { 3 } } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift index c767207..5ffb33c 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift @@ -54,10 +54,8 @@ public struct RuntimeService { } } - public func installRuntimeImage(dmgURL: URL) throws { - Task { - _ = try await Current.shell.installRuntimeImage(dmgURL) - } + public func installRuntimeImage(dmgURL: URL) async throws { + _ = try await Current.shell.installRuntimeImage(dmgURL) } public func mountDMG(dmgUrl: URL) async throws -> URL { @@ -72,13 +70,20 @@ public struct RuntimeService { } public func unmountDMG(mountedURL: URL) async throws { - let url = try await Current.shell.unmountDmg(mountedURL) + _ = try await Current.shell.unmountDmg(mountedURL) } public func expand(pkgPath: Path, expandedPkgPath: Path) async throws { _ = try await Current.shell.expandPkg(pkgPath.url, expandedPkgPath.url) } - + + public func createPkg(pkgPath: Path, expandedPkgPath: Path) async throws { + _ = try await Current.shell.createPkg(pkgPath.url, expandedPkgPath.url) + } + + public func installPkg(pkgPath: Path, expandedPkgPath: Path) async throws { + _ = try await Current.shell.installPkg(pkgPath.url, expandedPkgPath.url.absoluteString) + } } extension String: Error {} From c153a93b1b5a5e21371dd58118769fe28dd186df Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Thu, 23 Nov 2023 14:41:56 -0600 Subject: [PATCH 6/7] adjustments to update runtimes --- Xcodes.xcodeproj/project.pbxproj | 6 +- Xcodes/Backend/AppState+Runtimes.swift | 2 + Xcodes/Backend/AppState.swift | 2 +- Xcodes/Frontend/InfoPane/InfoPane.swift | 12 +++- Xcodes/Frontend/InfoPane/RuntimesView.swift | 66 +++++++++++++++++++++ 5 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 Xcodes/Frontend/InfoPane/RuntimesView.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index b73e456..22bf2c6 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; }; 36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; }; - 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; }; + 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; }; 536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */; }; 53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CBAB2B263DCC9100410495 /* XcodesAlert.swift */; }; 63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EAA4EA259944450046AB8F /* ProgressButton.swift */; }; @@ -113,6 +113,7 @@ E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; }; E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; }; E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; }; + E84CF8C12B0FEB8300ECA259 /* RuntimesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */; }; E872EE4E2808D4F100D3DD8B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E872EE502808D4F100D3DD8B /* Localizable.strings */; }; E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; }; E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; }; @@ -324,6 +325,7 @@ E2AFDCCA28F024D000864ADD /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = ""; }; E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = ""; }; + E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimesView.swift; sourceTree = ""; }; E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = ""; }; E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = ""; }; @@ -659,6 +661,7 @@ B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */, B0C6AD0C2AD91D7900E64698 /* IconView.swift */, E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */, + E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */, ); path = InfoPane; sourceTree = ""; @@ -880,6 +883,7 @@ CA9FF8CF25959A9700E47BAF /* HelperXPCShared.swift in Sources */, CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */, CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */, + E84CF8C12B0FEB8300ECA259 /* RuntimesView.swift in Sources */, CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */, CAA8589B25A2B83000ACF8C0 /* Aria2CError.swift in Sources */, 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */, diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index 4c19820..be8e088 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -54,6 +54,8 @@ extension AppState { guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return } self.downloadableRuntimes[index].installState = .installed } + + updateInstalledRuntimes() } catch { Logger.appState.error("Error downloading runtime: \(error.localizedDescription)") diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index cd6b4ef..9db4dbe 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -824,7 +824,7 @@ class AppState: ObservableObject { // MARK: Runtimes func runtimeInstallPath(xcode: Xcode, runtime: DownloadableRuntime) -> Path? { - if let coreSimulatorInfo = installedRuntimes.filter({ $0.runtimeInfo.build == runtime.sdkBuildUpdate }).first { + if let coreSimulatorInfo = installedRuntimes.filter({ $0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate }).first { let urlString = coreSimulatorInfo.path["relative"]! // app was not allowed to open up file:// url's so remove let fileRemovedString = urlString.replacingOccurrences(of: "file://", with: "") diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index 3fcde23..d39648a 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -22,8 +22,9 @@ struct InfoPane: View { Divider() Group { - ReleaseNotesView(url: xcode.releaseNotesURL) + RuntimesView(xcode: xcode) ReleaseDateView(date: xcode.releaseDate) + ReleaseNotesView(url: xcode.releaseNotesURL) IdenticalBuildsView(builds: xcode.identicalBuilds) CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion) SDKsView(sdks: xcode.sdks) @@ -44,8 +45,13 @@ struct InfoPane: View { private func makePreviewContent(for index: Int) -> some View { let name = PreviewName.allCases[index] - - }) + return InfoPane(xcode: xcodeDict[name]!) + .environmentObject(configure(AppState()) { + $0.allXcodes = [xcodeDict[name]!] + }) + .frame(width: 300, height: 400) + .padding() +} enum PreviewName: String, CaseIterable, Identifiable { case Populated_Installed_Selected diff --git a/Xcodes/Frontend/InfoPane/RuntimesView.swift b/Xcodes/Frontend/InfoPane/RuntimesView.swift new file mode 100644 index 0000000..9d9251e --- /dev/null +++ b/Xcodes/Frontend/InfoPane/RuntimesView.swift @@ -0,0 +1,66 @@ +// +// RuntimesView.swift +// Xcodes +// +// Created by Matt Kiazyk on 2023-11-23. +// Copyright © 2023 Robots and Pencils. All rights reserved. +// + +import SwiftUI + +struct RuntimesView: View { + @EnvironmentObject var appState: AppState + let xcode: Xcode + + var body: some View { + VStack(alignment: .leading) { + Text("Platforms") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + + let builds = xcode.sdks?.allBuilds() + let runtimes = builds?.flatMap { sdkBuild in + appState.downloadableRuntimes.filter { + $0.sdkBuildUpdate == sdkBuild + } + } + // let runtimes = appState.getRunTimes(xcode: xcode) + + ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in + VStack { + HStack { + Text("\(runtime.visibleIdentifier)") + .font(.subheadline) + Spacer() + Text(runtime.downloadFileSizeString) + .font(.subheadline) + + // it's installed if we have a path + if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) { + Button(action: { appState.reveal(path: path.string) }) { + Image(systemName: "arrow.right.circle.fill") + } + .buttonStyle(PlainButtonStyle()) + .help("RevealInFinder") + } else { + DownloadRuntimeButton(runtime: runtime) + } + } + switch runtime.installState { + + case .installing(let installationStep): + RuntimeInstallationStepDetailView(installationStep: installationStep) + .fixedSize(horizontal: false, vertical: true) + default: + EmptyView() + } + } + + } + } + } +} + +//#Preview { +// RuntimesView() +//} From c1836a78c40dc601a1ba3ef2f31185e1739cd368 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Fri, 1 Dec 2023 10:05:36 -0600 Subject: [PATCH 7/7] PR Cleanup. --- Xcodes/Backend/AppState+Runtimes.swift | 27 -------- Xcodes/Backend/Environment.swift | 69 +-------------------- Xcodes/Frontend/InfoPane/RuntimesView.swift | 1 - 3 files changed, 2 insertions(+), 95 deletions(-) diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index be8e088..009279d 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -65,26 +65,6 @@ extension AppState { } } } - -// 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 { -// Logger.appState.error("Error downloading runtime: \(error.localizedDescription)") -//// // 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) async throws { @@ -126,7 +106,6 @@ extension AppState { // 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: "."))" let url = URL(string: runtime.source)! let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)" // aria2 downloads directly to the destination (instead of into /tmp first) so we need to make sure that the download isn't incomplete @@ -158,12 +137,6 @@ extension AppState { return Just(runtime.url) .setFailureType(to: Error.self) .eraseToAnyPublisher() - -// return downloadRuntimeWithURLSession( -// runtime, -// to: expectedRuntimePath, -// progressChanged: progressChanged -// ) } } } diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index 3784891..6d77108 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -111,74 +111,9 @@ public struct Shell { return (progress, publisher) } - + // TODO: Support using aria2 using AysncStream/AsyncSequence // 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) { -// throw aria2cError -// } else { -// throw ProcessExecutionError(process: process, standardOutput: "", standardError: "") -// } -// } -// return -// } -// } -// try process.run() -// } catch { -// throw error -// } -// } + public var unxipExperiment: (URL) -> AnyPublisher = { url in let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)! diff --git a/Xcodes/Frontend/InfoPane/RuntimesView.swift b/Xcodes/Frontend/InfoPane/RuntimesView.swift index 9d9251e..e9e894e 100644 --- a/Xcodes/Frontend/InfoPane/RuntimesView.swift +++ b/Xcodes/Frontend/InfoPane/RuntimesView.swift @@ -24,7 +24,6 @@ struct RuntimesView: View { $0.sdkBuildUpdate == sdkBuild } } - // let runtimes = appState.getRunTimes(xcode: xcode) ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in VStack {