diff --git a/README.md b/README.md index 0d3a600..301c241 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,13 @@ _If you're looking for a command-line version of Xcodes.app, try [`xcodes`](http - View release notes, OS compatibility, included SDKs and compilers from [Xcode Releases](https://xcodereleases.com). - Dark/Light Mode supported +## Experiments + +- Thanks to the wonderful work of [https://github.com/saagarjha/unxip](https://github.com/saagarjha/unxip), turn on the experiment to increase your unxipping time by up to 70%! More can be found on his repo, but bugs, high memory may occur if used. + +![](experiment_light.png#gh-light-mode-only) +![](experiment_dark.png#gh-dark-mode-only) + ## Installation Xcodes.app runs on macOS Big Sur 11.0 or later. @@ -80,7 +87,10 @@ scripts/package_release.sh # Notarize the app # Do this from the Product directory so the app is zipped without being nested inside Product # Create a app specific password on appleid.apple.com if you haven't already -# % xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "" -p +# xcrun notarytool store-credentials "AC_PASSWORD" \ +# --apple-id "test@example.com" \ +# --team-id "teamid" \ +# --password "app specific password" pushd Product ../scripts/notarize.sh "test@example.com" "@keychain:AC_PASSWORD" Xcodes.zip diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 9d79db5..bb2de18 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -104,6 +104,9 @@ E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; }; E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; }; E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; }; + E8CBDB8727ADD92000B22292 /* unxip in Resources */ = {isa = PBXBuildFile; fileRef = E8CBDB8627ADD92000B22292 /* unxip */; }; + E8CBDB8927ADE32300B22292 /* unxip in Copy aria2c */ = {isa = PBXBuildFile; fileRef = E8CBDB8627ADD92000B22292 /* unxip */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + E8CBDB8B27AE02FF00B22292 /* ExperiementsPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */; }; 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 */; }; @@ -153,6 +156,7 @@ dstPath = ""; dstSubfolderSpec = 6; files = ( + E8CBDB8927ADE32300B22292 /* unxip in Copy aria2c */, CAA8589325A2B77E00ACF8C0 /* aria2c in Copy aria2c */, ); name = "Copy aria2c"; @@ -272,6 +276,8 @@ 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 = ""; }; + E8CBDB8627ADD92000B22292 /* unxip */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = unxip; sourceTree = ""; }; + E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiementsPreferencePane.swift; sourceTree = ""; }; E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -484,6 +490,7 @@ CA9FF83E2594FBC000E47BAF /* Licenses.rtf */, CAD2E7AE2449575000113D76 /* Xcodes.entitlements */, CA8FB64D256E17B100469DA5 /* XcodesTest.entitlements */, + E8CBDB8627ADD92000B22292 /* unxip */, ); path = Resources; sourceTree = ""; @@ -561,6 +568,7 @@ CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */, E8977EA225C11E1500835F80 /* PreferencesView.swift */, E8DA461025FAF7FB002E85EF /* NotificationsView.swift */, + E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */, ); path = Preferences; sourceTree = ""; @@ -707,6 +715,7 @@ files = ( CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */, CA9FF83F2594FBC000E47BAF /* Licenses.rtf in Resources */, + E8CBDB8727ADD92000B22292 /* unxip in Resources */, CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */, CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */, ); @@ -785,6 +794,7 @@ 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 */, CAD2E7A42449574E00113D76 /* XcodeListView.swift in Sources */, diff --git a/Xcodes/AcknowledgementsGenerator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/Xcodes/AcknowledgementsGenerator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Xcodes/AcknowledgementsGenerator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 28449ee..a60a91c 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -238,8 +238,8 @@ extension AppState { func unarchiveAndMoveXIP(availableXcode: AvailableXcode, at source: URL, to destination: URL) -> AnyPublisher { self.setInstallationStep(of: availableXcode.version, to: .unarchiving) - - return Current.shell.unxip(source) + + return unxipOrUnxipExperiment(source) .catch { error -> AnyPublisher in if let executionError = error as? ProcessExecutionError { if executionError.standardError.contains("damaged and can’t be expanded") { @@ -278,6 +278,16 @@ extension AppState { }) .eraseToAnyPublisher() } + + func unxipOrUnxipExperiment(_ source: URL) -> AnyPublisher { + if unxipExperiment { + // All hard work done by https://github.com/saagarjha/unxip + // Compiled to binary with `swiftc -parse-as-library -O unxip.swift` + return Current.shell.unxipExperiment(source) + } else { + return Current.shell.unxip(source) + } + } public func verifySecurityAssessment(of xcode: InstalledXcode) -> AnyPublisher { return Current.shell.spctlAssess(xcode.path.url) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 78755e6..db190ca 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -62,6 +62,12 @@ class AppState: ObservableObject { Current.defaults.set(localPath, forKey: "localPath") } } + + @Published var unxipExperiment = false { + didSet { + Current.defaults.set(unxipExperiment, forKey: "unxipExperiment") + } + } // MARK: - Publisher Cancellables @@ -96,6 +102,7 @@ class AppState: ObservableObject { func setupDefaults() { localPath = Current.defaults.string(forKey: "localPath") ?? Path.defaultXcodesApplicationSupport.string + unxipExperiment = Current.defaults.bool(forKey: "unxipExperiment") ?? false } // MARK: Timer diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index ef93c4a..de3b6b4 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -111,6 +111,12 @@ public struct Shell { return (progress, publisher) } + + public var unxipExperiment: (URL) -> AnyPublisher = { url in + let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)! + return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"]) + } + } public struct Files { @@ -238,6 +244,11 @@ public struct Defaults { public func get(forKey key: String) -> Any? { get(key) } + + public var bool: (String) -> Bool? = { UserDefaults.standard.bool(forKey: $0) } + public func bool(forKey key: String) -> Bool? { + bool(key) + } } private let helperClient = HelperClient() diff --git a/Xcodes/Frontend/About/AboutView.swift b/Xcodes/Frontend/About/AboutView.swift index a34a817..b89004b 100644 --- a/Xcodes/Frontend/About/AboutView.swift +++ b/Xcodes/Frontend/About/AboutView.swift @@ -14,9 +14,6 @@ struct AboutView: View { Text("Version \(Bundle.main.shortVersion!) (\(Bundle.main.version!))") - Color.clear - .frame(width: 300, height: 16) - HStack(spacing: 32) { Button(action: { openURL(URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!) @@ -30,6 +27,24 @@ struct AboutView: View { } .buttonStyle(LinkButtonStyle()) } + Color.clear + .frame(width: 300, height: 0) + Label("Unxip Experiment", systemImage: "testtube.2") + HStack(spacing: 32) { + Button(action: { + openURL(URL(string: "https://github.com/saagarjha/unxip/")!) + }) { + Label("Github Repo", systemImage: "link") + } + .buttonStyle(LinkButtonStyle()) + + Button(action: { + openURL(URL(string: "https://github.com/saagarjha/unxip/blob/main/LICENSE")!) + }) { + Label("License", systemImage: "link") + } + .buttonStyle(LinkButtonStyle()) + } Text(Bundle.main.humanReadableCopyright!) .font(.footnote) diff --git a/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift b/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift new file mode 100644 index 0000000..1bc04e0 --- /dev/null +++ b/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift @@ -0,0 +1,55 @@ +import AppleAPI +import SwiftUI +import Path + +struct ExperimentsPreferencePane: View { + @EnvironmentObject var appState: AppState + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + GroupBox(label: Text("Faster Unxip")) { + VStack(alignment: .leading) { + Toggle( + "When unxipping, use experiment", + isOn: $appState.unxipExperiment + ) + AttributedText(unxipFootnote) + } + .fixedSize(horizontal: false, vertical: true) + } + .groupBoxStyle(PreferencesGroupBoxStyle()) + + Divider() + } + .frame(width: 500) + } + + private var unxipFootnote: NSAttributedString { + let string = """ + Thanks to @_saagarjha, this experiment can increase unxipping + speed by up to 70% for some systems. + + More information on how this is accomplished can be seen + on the unxip repo - https://github.com/saagarjha/unxip + """ + let attributedString = NSMutableAttributedString( + string: string, + attributes: [ + .font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]), + .foregroundColor: NSColor.labelColor + ] + ) + attributedString.addAttribute(.link, value: URL(string: "https://twitter.com/_saagarjha")!, range: NSRange(string.range(of: "@_saagarjha")!, in: string)) + attributedString.addAttribute(.link, value: URL(string: "https://github.com/saagarjha/unxip")!, range: NSRange(string.range(of: "https://github.com/saagarjha/unxip")!, in: string)) + return attributedString + } +} + +struct ExperimentsPreferencePane_Previews: PreviewProvider { + static var previews: some View { + Group { + ExperimentsPreferencePane() + .environmentObject(AppState()) + } + } +} diff --git a/Xcodes/Frontend/Preferences/PreferencesView.swift b/Xcodes/Frontend/Preferences/PreferencesView.swift index 8e896aa..b4cbc2c 100644 --- a/Xcodes/Frontend/Preferences/PreferencesView.swift +++ b/Xcodes/Frontend/Preferences/PreferencesView.swift @@ -2,7 +2,7 @@ import SwiftUI struct PreferencesView: View { private enum Tabs: Hashable { - case general, updates, advanced + case general, updates, advanced, experiment } @EnvironmentObject var appState: AppState @@ -25,6 +25,11 @@ struct PreferencesView: View { Label("Advanced", systemImage: "gearshape.2") } .tag(Tabs.advanced) + ExperimentsPreferencePane() + .tabItem { + Label("Experiments", systemImage: "testtube.2") + } + .tag(Tabs.experiment) } .padding(20) } diff --git a/Xcodes/Resources/Info.plist b/Xcodes/Resources/Info.plist index 207b615..06877b9 100644 --- a/Xcodes/Resources/Info.plist +++ b/Xcodes/Resources/Info.plist @@ -25,7 +25,7 @@ LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copyright © 2020 Robots and Pencils. + Copyright © 2022 Robots and Pencils. NSPrincipalClass NSApplication NSSupportsAutomaticTermination @@ -35,7 +35,7 @@ SMPrivilegedExecutables com.robotsandpencils.XcodesApp.Helper - identifier "com.robotsandpencils.XcodesApp.Helper" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)" + identifier "com.robotsandpencils.XcodesApp.Helper" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)" SUFeedURL https://robotsandpencils.github.io/XcodesApp/appcast.xml diff --git a/Xcodes/Resources/unxip b/Xcodes/Resources/unxip new file mode 100755 index 0000000..fb17685 Binary files /dev/null and b/Xcodes/Resources/unxip differ diff --git a/experiment_dark.jpg b/experiment_dark.jpg new file mode 100644 index 0000000..b71f79b Binary files /dev/null and b/experiment_dark.jpg differ diff --git a/experiment_light.jpg b/experiment_light.jpg new file mode 100644 index 0000000..a547328 Binary files /dev/null and b/experiment_light.jpg differ