Merge pull request #448 from XcodesOrg/matt/runtimeDownload

Support Runtime/Platforms Downloading and Install 🚀
This commit is contained in:
Matt Kiazyk 2023-12-01 10:06:45 -06:00 committed by GitHub
commit c5ada02a31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1187 additions and 85 deletions

View file

@ -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 = "<group>"; };
CA378F982466567600A58CE0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = "<group>"; };
CA42DD6D25AEA8B200BC0B0C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
CA42DD7225AEB04300BC0B0C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = "<group>"; };
@ -289,7 +292,6 @@
CABFAA422593104F00380FEE /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+InfoPlistValues.swift"; sourceTree = "<group>"; };
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingProgressIndicator.swift; sourceTree = "<group>"; };
CAC281D9259F985100B8AB0B /* InstallationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStep.swift; sourceTree = "<group>"; };
CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+XcodesTests.swift"; sourceTree = "<group>"; };
CAC281E6259FA45A00B8AB0B /* Environment+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Mock.swift"; sourceTree = "<group>"; };
CAC9F92C25BCDA4400B4965F /* HelperInstallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperInstallState.swift; sourceTree = "<group>"; };
@ -322,11 +324,16 @@
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
E2AFDCCA28F024D000864ADD /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = "<group>"; };
E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = "<group>"; };
E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimesView.swift; sourceTree = "<group>"; };
E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = "<group>"; };
E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = "<group>"; };
E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = "<group>"; };
E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDKs+Xcode.swift"; sourceTree = "<group>"; };
E8C0EB1B291EF9A10081528A /* AppState+Runtimes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+Runtimes.swift"; sourceTree = "<group>"; };
E8CBDB8627ADD92000B22292 /* unxip */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = unxip; sourceTree = "<group>"; };
E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiementsPreferencePane.swift; sourceTree = "<group>"; };
E8D0296E284B029800647641 /* BottomStatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomStatusBar.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 = "<group>";
@ -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 */;

View file

@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "AsyncNetworkService",
"repositoryURL": "https://github.com/RobotsAndPencils/AsyncHTTPNetworkService",
"state": {
"branch": "main",
"revision": "97770856c4e429f880d4b4dd68cfaf286dc00c30",
"version": null
}
},
{
"package": "CombineExpectations",
"repositoryURL": "https://github.com/groue/CombineExpectations",
@ -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"
}
},
{

View file

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

View file

@ -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<URL, Error> {
// Check to see if the dmg is in the expected path in case it was downloaded but failed to install
// call https://developerservices2.apple.com/services/download?path=/Developer_Tools/watchOS_10_beta/watchOS_10_beta_Simulator_Runtime.dmg 1st to get cookie
// use runtime.url for final with cookies
// Check to see if the archive is in the expected path in case it was downloaded but failed to install
let 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<URL, Error> {
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: runtime.url) ?? []
let (progress, publisher) = Current.shell.downloadWithAria2(
aria2Path,
runtime.url,
destination,
cookies
)
progressChanged(progress)
return publisher
.map { _ in destination.url }
.eraseToAnyPublisher()
}
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))
}
}
}
}

View file

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

View file

@ -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<AnyCancellable>()
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<Void, Error> {
return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path))
.receive(on: DispatchQueue.main)
.tryMap { _ in
.tryMap { result -> Void in
let httpResponse = result.response as! HTTPURLResponse
if httpResponse.statusCode == 401 {
throw AuthenticationError.notAuthorized
}
}
.eraseToAnyPublisher()
}
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<Void, Error> {
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<Void, Error> {

View file

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

View file

@ -3,7 +3,7 @@ import Foundation
import Path
import AppleAPI
import KeychainAccess
import XcodesKit
/**
Lightweight dependency injection using global mutable state :P
@ -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<ProcessOutput, Error> = { 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<URLSession.DataTaskPublisher.Output, Error> {
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>) {

View file

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

View file

@ -28,4 +28,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
}
}

View file

@ -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<ProcessOutput, Error> {
static func run(_ executable: any Pathish, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher<ProcessOutput, Error> {
return run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
}
@ -67,9 +68,3 @@ extension Process {
.eraseToAnyPublisher()
}
}
struct ProcessExecutionError: Error {
let process: Process
let standardOutput: String
let standardError: String
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
import SwiftUI
import XcodesKit
struct InstallationStepRowView: View {
let installationStep: InstallationStep
let installationStep: XcodeInstallationStep
let highlighted: Bool
let cancel: () -> Void

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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)
}
}

View file

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

View file

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

View file

@ -4,6 +4,8 @@ import CombineExpectations
import Path
import Version
import XCTest
import XcodesKit
@testable import Xcodes
class AppStateTests: XCTestCase {

View file

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