diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 4ce7a310..22bf2c6d 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 */; }; @@ -29,7 +29,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 */; }; @@ -82,7 +81,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 */; }; @@ -92,7 +90,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 */; }; @@ -115,11 +112,16 @@ 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 */; }; + 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 */; }; 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 */; }; @@ -127,7 +129,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 */ @@ -221,7 +225,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 = ""; }; @@ -289,7 +292,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 = ""; }; @@ -322,11 +324,16 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -354,9 +361,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; }; @@ -478,6 +487,7 @@ isa = PBXGroup; children = ( CA378F982466567600A58CE0 /* AppState.swift */, + E8C0EB1B291EF9A10081528A /* AppState+Runtimes.swift */, CAE424B3259A764700B8B246 /* AppState+Install.swift */, CABFA9A72592EEE900380FEE /* AppState+Update.swift */, CAA8589A25A2B83000ACF8C0 /* Aria2CError.swift */, @@ -496,10 +506,8 @@ CABFA9AC2592EEE900380FEE /* Foundation.swift */, CA9FF9352595B44700E47BAF /* HelperClient.swift */, CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */, - CAC281D9259F985100B8AB0B /* InstallationStep.swift */, CA9FF8862595607900E47BAF /* InstalledXcode.swift */, CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */, - CA42DD6D25AEA8B200BC0B0C /* Logger.swift */, E89342F925EDCC17007CF557 /* NotificationManager.swift */, CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */, CABFA9AE2592EEE900380FEE /* Path+.swift */, @@ -518,6 +526,7 @@ E81D7E9F2805250100A205FC /* Collection+.swift */, E8D655BF288DD04700A139C2 /* SelectedActionType.swift */, E87AB3C42939B65E00D72F43 /* Hardware.swift */, + E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */, ); path = Backend; sourceTree = ""; @@ -558,6 +567,7 @@ CAD2E7952449574E00113D76 = { isa = PBXGroup; children = ( + E856BB73291EDD3D00DC438B /* XcodesKit */, CA8FB5F8256E0F9400469DA5 /* README.md */, CABFA9D42592EF6300380FEE /* DECISIONS.md */, CABFA9A02592EAF500380FEE /* R&PLogo.png */, @@ -650,6 +660,8 @@ B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */, B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */, B0C6AD0C2AD91D7900E64698 /* IconView.swift */, + E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */, + E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */, ); path = InfoPane; sourceTree = ""; @@ -693,7 +705,6 @@ name = Xcodes; packageProductDependencies = ( CAA1CB2C255A5262003FD669 /* AppleAPI */, - CABFA9DE2592F07A00380FEE /* Path */, CABFA9E32592F08E00380FEE /* Version */, CABFA9ED2592F0CC00380FEE /* SwiftSoup */, CABFA9F72592F0F900380FEE /* KeychainAccess */, @@ -702,6 +713,9 @@ CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */, E8F81FC3282D8A17006CBD0F /* Sparkle */, E689540225BE8C64000EBCEA /* DockProgress */, + E8FD5726291EE4AC001E004C /* AsyncNetworkService */, + E8C0EB19291EF43E0081528A /* XcodesKit */, + E8F44A1D296B4CD7002D6592 /* Path */, ); productName = XcodesMac; productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */; @@ -777,7 +791,6 @@ ); mainGroup = CAD2E7952449574E00113D76; packageReferences = ( - CABFA9DD2592F07A00380FEE /* XCRemoteSwiftPackageReference "Path" */, CABFA9E22592F08E00380FEE /* XCRemoteSwiftPackageReference "Version" */, CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */, CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */, @@ -787,6 +800,8 @@ CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */, E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */, E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */, + E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */, + E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */, ); productRefGroup = CAD2E79F2449574E00113D76 /* Products */; projectDirPath = ""; @@ -868,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 */, @@ -886,7 +902,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 */, @@ -900,13 +915,13 @@ B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */, B0403CFE2ADA712C00137C09 /* InfoPaneControls.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 */, B0403CF02AD92D7B00137C09 /* ReleaseNotesView.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 */, @@ -918,6 +933,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 */, @@ -946,6 +962,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 */, @@ -1095,7 +1112,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; @@ -1336,7 +1353,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; }; @@ -1360,7 +1377,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; }; @@ -1458,8 +1475,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/xcodereleases/data"; requirement = { - kind = revision; - revision = a43ad89e536d7a3da525fcc23fb182c37b756ecc; + branch = main; + kind = branch; }; }; CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */ = { @@ -1470,14 +1487,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"; @@ -1526,6 +1535,14 @@ minimumVersion = 3.2.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"; @@ -1534,6 +1551,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 */ @@ -1551,11 +1576,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" */; @@ -1586,11 +1606,25 @@ package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */; productName = DockProgress; }; + 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 02ebe90e..a08a9b21 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", @@ -14,7 +23,7 @@ "package": "XcodeReleases", "repositoryURL": "https://github.com/xcodereleases/data", "state": { - "branch": null, + "branch": "main", "revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc", "version": null } @@ -60,8 +69,8 @@ "repositoryURL": "https://github.com/mxcl/Path.swift", "state": { "branch": null, - "revision": "dac007e907a4f4c565cfdc55a9ce148a761a11d5", - "version": "0.16.3" + "revision": "8e355c28e9393c42e58b18c54cace2c42c98a616", + "version": "1.4.1" } }, { diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 416d38e6..ae562d30 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -6,6 +6,7 @@ import Version import LegibleError import os.log import DockProgress +import XcodesKit /// Downloads and installs Xcodes extension AppState { @@ -489,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) @@ -498,6 +499,15 @@ 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) + + Current.notificationManager.scheduleNotification(title: runtime.name, body: step.description, category: .normal) + } + } } extension AppState { diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift new file mode 100644 index 00000000..009279df --- /dev/null +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -0,0 +1,183 @@ +import Foundation +import XcodesKit +import OSLog +import Combine +import Path +import AppleAPI + +extension AppState { + func updateDownloadableRuntimes() { + Task { + do { + + 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 + } + 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)") + } + } + } + + func downloadRuntime(runtime: DownloadableRuntime) { + 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 + } + + updateInstalledRuntimes() + } + 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) + } + } + } + } + + 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)") + + + 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() + + Logger.appState.debug("Done downloading: \(url)") + DispatchQueue.main.async { + self.setInstallationStep(of: runtime, to: .installing) + } + switch runtime.contentType { + case .package: + // 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 + + // 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 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 + 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 { + + Logger.appState.info("Downloading runtime: \(url.lastPathComponent)") + switch downloader { + case .aria2: + let aria2Path = Path(url: Bundle.main.url(forAuxiliaryExecutable: "aria2c")!)! + return downloadRuntimeWithAria2( + runtime, + to: expectedRuntimePath, + aria2Path: aria2Path, + progressChanged: progressChanged) + + case .urlSession: + // TODO: Support runtime download via URL Session + return Just(runtime.url) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + } + } + + 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() + } + + public func installFromImage(dmgURL: URL) async throws { + try await self.runtimeService.installRuntimeImage(dmgURL: dmgURL) + } +} + +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/AppState+Update.swift b/Xcodes/Backend/AppState+Update.swift index 8dad1725..90f531f1 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 168369f2..9db4dbe2 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -8,9 +8,11 @@ import Path import Version import os.log import DockProgress +import XcodesKit class AppState: ObservableObject { private let client = AppleAPI.Client() + internal let runtimeService = RuntimeService() // MARK: - Published Properties @@ -100,10 +102,17 @@ class AppState: ObservableObject { Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption") } } + + // MARK: - Runtimes + + @Published var downloadableRuntimes: [DownloadableRuntime] = [] + @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? @@ -150,9 +159,11 @@ class AppState: ObservableObject { init() { guard !isTesting else { return } try? loadCachedAvailableXcodes() + try? loadCacheDownloadableRuntimes() checkIfHelperIsInstalled() setupAutoInstallTimer() setupDefaults() + updateInstalledRuntimes() } func setupDefaults() { @@ -180,11 +191,23 @@ 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() } + 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,6 +822,19 @@ class AppState: ObservableObject { self.allXcodes = newAllXcodes.sorted { $0.version > $1.version } } + // MARK: Runtimes + func runtimeInstallPath(xcode: Xcode, runtime: DownloadableRuntime) -> Path? { + 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: "") + let url = URL(fileURLWithPath: fileRemovedString) + + return Path(url: url)! + } + return nil + } + // MARK: - Private private func uninstallXcode(path: Path) -> AnyPublisher { diff --git a/Xcodes/Backend/Entry+.swift b/Xcodes/Backend/Entry+.swift index c6e8c118..b195fb0b 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 9a43ec83..6d771086 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 @@ -111,6 +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 + public var unxipExperiment: (URL) -> AnyPublisher = { url in let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)! @@ -166,18 +169,24 @@ 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 } } + + 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] { - ((try? destination.ls()) ?? []) + destination.ls() .filter { $0.isAppBundle && $0.infoPlist?.bundleID == "com.apple.dt.Xcode" } - .map { $0.path } + .map { $0 } .compactMap(InstalledXcode.init) } @@ -189,10 +198,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/Logger.swift b/Xcodes/Backend/Logger.swift deleted file mode 100644 index 57540b82..00000000 --- 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 36e00421..06bbe63e 100644 --- a/Xcodes/Backend/Path+.swift +++ b/Xcodes/Backend/Path+.swift @@ -28,4 +28,19 @@ extension Path { } return 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/Backend/Process.swift b/Xcodes/Backend/Process.swift index fe960013..b3e5a64f 100644 --- a/Xcodes/Backend/Process.swift +++ b/Xcodes/Backend/Process.swift @@ -2,12 +2,13 @@ import Combine import Foundation import os.log import Path +import XcodesKit public typealias ProcessOutput = (status: Int32, out: String, err: String) 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 +68,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 00000000..1462b963 --- /dev/null +++ b/Xcodes/Backend/SDKs+Xcode.swift @@ -0,0 +1,35 @@ +// +// 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 + } + if let visionOS = self.visionOS?.compactMap({ $0.build }) { + buildNumbers += visionOS + } + + return buildNumbers + } +} diff --git a/Xcodes/Backend/Xcode.swift b/Xcodes/Backend/Xcode.swift index 603e35d0..4003ac2d 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 6767afec..ce363999 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -1,4 +1,5 @@ import SwiftUI +import XcodesKit // MARK: - CommandMenu @@ -208,6 +209,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.downloadRuntime(runtime: runtime) + } +} + struct CreateSymbolicBetaLinkButton: View { @EnvironmentObject var appState: AppState let xcode: Xcode? diff --git a/Xcodes/Backend/XcodeInstallState.swift b/Xcodes/Backend/XcodeInstallState.swift index a02181ef..a289bb59 100644 --- a/Xcodes/Backend/XcodeInstallState.swift +++ b/Xcodes/Backend/XcodeInstallState.swift @@ -1,9 +1,10 @@ import Foundation import Path +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/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index e1215c87..d39648a7 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,13 +45,12 @@ 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() + return InfoPane(xcode: xcodeDict[name]!) + .environmentObject(configure(AppState()) { + $0.allXcodes = [xcodeDict[name]!] + }) + .frame(width: 300, height: 400) + .padding() } enum PreviewName: String, CaseIterable, Identifiable { diff --git a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift index f9fb7d8c..75339ab4 100644 --- a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift +++ b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift @@ -1,7 +1,8 @@ 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/InfoPane/RuntimeInstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift new file mode 100644 index 00000000..f59f0417 --- /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/Frontend/InfoPane/RuntimesView.swift b/Xcodes/Frontend/InfoPane/RuntimesView.swift new file mode 100644 index 00000000..e9e894e6 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/RuntimesView.swift @@ -0,0 +1,65 @@ +// +// 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 + } + } + + 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() +//} diff --git a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift index e2e81f16..3bf7db56 100644 --- a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift +++ b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift @@ -1,7 +1,8 @@ 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 095deef0..179cdf42 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -1,10 +1,37 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2639 +{\rtf1\ansi\ansicpg1252\cocoartf2758 \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 15513a63..66b4ace5 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 00000000..3b298120 --- /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 00000000..81447abf --- /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 00000000..5312c492 --- /dev/null +++ b/Xcodes/XcodesKit/README.md @@ -0,0 +1,3 @@ +# XcodesKit + +A description of this package. diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Foundation.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Foundation.swift new file mode 100644 index 00000000..02b1e022 --- /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 00000000..222e9080 --- /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/Runtimes/CoreSimulatorImage.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift new file mode 100644 index 00000000..a85d3c89 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift @@ -0,0 +1,22 @@ +// +// 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 path: [String: String] + public let runtimeInfo: CoreSimulatorRuntimeInfo +} + +public struct CoreSimulatorRuntimeInfo: Decodable { + public let build: String +} 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 00000000..84c4c8d1 --- /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 + + 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 00000000..27b4e175 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift @@ -0,0 +1,39 @@ +// +// RuntimeInstallationStep.swift +// +// +// Created by Matt Kiazyk on 2023-11-23. +// + +import Foundation + +public enum RuntimeInstallationStep: Equatable, CustomStringConvertible { + case downloading(progress: Progress) + case installing + case trashingArchive + + public var description: String { + "(\(stepNumber)/\(stepCount)) \(message)" + } + + public var message: String { + switch self { + case .downloading: + return localizeString("Downloading") + case .installing: + return localizeString("Installing") + case .trashingArchive: + return localizeString("TrashingArchive") + } + } + + public var stepNumber: Int { + switch self { + case .downloading: return 1 + case .installing: return 2 + case .trashingArchive: return 3 + } + } + + public var stepCount: Int { 3 } +} 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 00000000..9e9e370b --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift @@ -0,0 +1,182 @@ +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? + public var url: URL { + return URL(string: source)! + } + public var downloadPath: String { + url.path + } + + // dynamically updated - not decoded + public var installState: RuntimeInstallState = .notInstalled + public var sdkBuildUpdate: String? + + 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 + case sdkBuildUpdate + } + + 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" + 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 + } + } + + var shortName: String { + switch self { + case .iOS: return "iOS" + case .macOS: return "macOS" + case .watchOS: return "watchOS" + case .tvOS: return "tvOS" + case .visionOS: return "visionOS" + } + } + } +} + +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" + 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/Models/XcodeInstallState.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallState.swift new file mode 100644 index 00000000..0f824a5a --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallState.swift @@ -0,0 +1,34 @@ +// +// InstallState.swift +// +// +// Created by Matt Kiazyk on 2023-06-06. +// + +import Foundation +import Path + +public enum XcodeInstallState: Equatable { + case notInstalled + case installing(XcodeInstallationStep) + 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/Backend/InstallationStep.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallationStep.swift similarity index 65% rename from Xcodes/Backend/InstallationStep.swift rename to Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallationStep.swift index ca2001d4..8d5513d3 100644 --- a/Xcodes/Backend/InstallationStep.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallationStep.swift @@ -1,7 +1,14 @@ +// +// InstallationStep.swift +// +// +// Created by Matt Kiazyk on 2023-06-06. +// + import Foundation -/// A numbered step -enum InstallationStep: Equatable, CustomStringConvertible { +// A numbered step +public enum XcodeInstallationStep: Equatable, CustomStringConvertible { case downloading(progress: Progress) case unarchiving case moving(destination: String) @@ -9,11 +16,11 @@ enum InstallationStep: Equatable, CustomStringConvertible { case checkingSecurity case finishing - var description: String { + public var description: String { "(\(stepNumber)/\(stepCount)) \(message)" } - var message: String { + public var message: String { switch self { case .downloading: return localizeString("Downloading") @@ -30,7 +37,7 @@ enum InstallationStep: Equatable, CustomStringConvertible { } } - var stepNumber: Int { + public var stepNumber: Int { switch self { case .downloading: return 1 case .unarchiving: return 2 @@ -41,5 +48,14 @@ enum InstallationStep: Equatable, CustomStringConvertible { } } - var stepCount: Int { 6 } + public 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/Services/RuntimeService.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift new file mode 100644 index 00000000..5ffb33c2 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift @@ -0,0 +1,89 @@ +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 enum Error: LocalizedError, Equatable { + case unavailableRuntime(String) + case failedMountingDMG + } + + 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 -> [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 + } catch { + throw error + } + } + + public func installRuntimeImage(dmgURL: URL) async throws { + _ = 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 { + _ = 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 {} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift new file mode 100644 index 00000000..b83a0798 --- /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/XcodesShell.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift new file mode 100644 index 00000000..c5760b3e --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift @@ -0,0 +1,26 @@ +import Foundation +import Path + +public struct XcodesShell { + 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) + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/XcodesKitEnvironment.swift b/Xcodes/XcodesKit/Sources/XcodesKit/XcodesKitEnvironment.swift new file mode 100644 index 00000000..c6a52435 --- /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/Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift b/Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift new file mode 100644 index 00000000..98f0c961 --- /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!") + } +} diff --git a/XcodesTests/AppStateTests.swift b/XcodesTests/AppStateTests.swift index ce38607c..4be9ca32 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 8755da53..f030d796 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,