From 9dd91a2ce85b80ea1379871b81f676a5718cccb7 Mon Sep 17 00:00:00 2001 From: Sam Lu Date: Sun, 24 Jan 2021 22:22:21 -0700 Subject: [PATCH 1/4] Add DockProgress swift package --- Xcodes.xcodeproj/project.pbxproj | 17 +++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++++++++ Xcodes/Resources/Licenses.rtf | 15 +++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 0fc83e8..3f47b6e 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; }; E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; }; E8F81FC4282D8A17006CBD0F /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E8F81FC3282D8A17006CBD0F /* Sparkle */; }; + E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -326,6 +327,7 @@ E8F81FC4282D8A17006CBD0F /* Sparkle in Frameworks */, CABFA9E42592F08E00380FEE /* Version in Frameworks */, CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */, + E689540325BE8C64000EBCEA /* DockProgress in Frameworks */, CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */, CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */, CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */, @@ -665,6 +667,7 @@ CA9FF86C25951C6E00E47BAF /* XCModel */, CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */, E8F81FC3282D8A17006CBD0F /* Sparkle */, + E689540225BE8C64000EBCEA /* DockProgress */, ); productName = XcodesMac; productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */; @@ -748,6 +751,7 @@ CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */, CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */, E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */, + E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */, ); productRefGroup = CAD2E79F2449574E00113D76 /* Products */; projectDirPath = ""; @@ -1475,6 +1479,14 @@ minimumVersion = 2.0.0; }; }; + E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/DockProgress"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 3.2.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1527,6 +1539,11 @@ package = E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; + E689540225BE8C64000EBCEA /* DockProgress */ = { + isa = XCSwiftPackageProductDependency; + package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */; + productName = DockProgress; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = CAD2E7962449574E00113D76 /* Project object */; diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 58f4e08..4e1ffc8 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,6 +19,15 @@ "version": null } }, + { + "package": "DockProgress", + "repositoryURL": "https://github.com/sindresorhus/DockProgress", + "state": { + "branch": null, + "revision": "7100b68571e2dafe3a06ad5905b80fc3b0107b4b", + "version": "3.2.0" + } + }, { "package": "ErrorHandling", "repositoryURL": "https://github.com/RobotsAndPencils/ErrorHandling", diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index 93bc0ff..095deef 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -308,6 +308,21 @@ For more information, please refer to <>\ otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ \ +\fs34 DockProgress\ +\ + +\fs26 MIT License\ +\ +Copyright (c) Sindre Sorhus (sindresorhus.com)\ +\ +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 KeychainAccess\ \ From de35bed9fad20f30834b36e04e0ff40d48f50b69 Mon Sep 17 00:00:00 2001 From: Sam Lu Date: Sun, 24 Jan 2021 22:58:12 -0700 Subject: [PATCH 2/4] Add dock progress indicator in .bar style --- Xcodes/Backend/AppState+Install.swift | 10 +++++++++- Xcodes/Backend/AppState.swift | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index ccf637d..d539263 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -5,6 +5,7 @@ import AppleAPI import Version import LegibleError import os.log +import DockProgress /// Downloads and installs Xcodes extension AppState { @@ -151,6 +152,7 @@ extension AppState { cookies ) progressChanged(progress) + updateDockIcon(withProgress: progress) return publisher .map { _ in destination.url } .eraseToAnyPublisher() @@ -160,11 +162,12 @@ extension AppState { let resumeDataPath = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).resumedata" let persistedResumeData = Current.files.contents(atPath: resumeDataPath.string) - return attemptResumableTask(maximumRetryCount: 3) { resumeData -> AnyPublisher in + return attemptResumableTask(maximumRetryCount: 3) { [weak self] resumeData -> AnyPublisher in let (progress, publisher) = Current.network.downloadTask(with: availableXcode.url, to: destination.url, resumingWith: resumeData ?? persistedResumeData) progressChanged(progress) + self?.updateDockIcon(withProgress: progress) return publisher .map { $0.saveLocation } .eraseToAnyPublisher() @@ -174,6 +177,11 @@ extension AppState { }) .eraseToAnyPublisher() } + + private func updateDockIcon(withProgress progress: Progress) { + DockProgress.style = .bar + DockProgress.progressInstance = progress + } public func installArchivedXcode(_ availableXcode: AvailableXcode, at archiveURL: URL) -> AnyPublisher { do { diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index ebd44b5..ca629f2 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -7,6 +7,7 @@ import KeychainAccess import Path import Version import os.log +import DockProgress class AppState: ObservableObject { private let client = AppleAPI.Client() @@ -489,6 +490,9 @@ class AppState: ObservableObject { // Cancel the publisher installationPublishers[id] = nil + + // Remove dock icon progress indicator + DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete // If the download is cancelled by the user, clean up the download files that aria2 creates. // This isn't done as part of the publisher with handleEvents(receiveCancel:) because it shouldn't happen when e.g. the app quits. From 622fac605c8c02cd3ee11a7e14aa7f2749ce797a Mon Sep 17 00:00:00 2001 From: Sam Lu Date: Fri, 15 Sep 2023 14:56:36 -0600 Subject: [PATCH 3/4] Set up overall progress object to track both downloading and unarchiving and reflect this in dock progress --- Xcodes/Backend/AppState+Install.swift | 40 +++++++++++++++++++++------ Xcodes/Backend/AppState.swift | 16 +++++++++-- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index d539263..416d38e 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -44,6 +44,8 @@ extension AppState { Logger.appState.info("Using \(downloader) downloader") + setupDockProgress() + return validateSession() .flatMap { _ in self.getXcodeArchive(installationType, downloader: downloader) @@ -52,6 +54,8 @@ extension AppState { self.installArchivedXcode(xcode, at: url) } .catch { error -> AnyPublisher in + self.resetDockProgressTracking() + switch error { case InstallationError.damagedXIP(let damagedXIPURL): guard attemptNumber < 1 else { return Fail(error: error).eraseToAnyPublisher() } @@ -100,6 +104,7 @@ extension AppState { self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in DispatchQueue.main.async { self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress)) + self.overallProgress.addChild(progress, withPendingUnitCount: AppState.totalProgressUnits - AppState.unxipProgressWeight) } }) .map { return (availableXcode, $0) } @@ -152,7 +157,7 @@ extension AppState { cookies ) progressChanged(progress) - updateDockIcon(withProgress: progress) + return publisher .map { _ in destination.url } .eraseToAnyPublisher() @@ -162,12 +167,12 @@ extension AppState { let resumeDataPath = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).resumedata" let persistedResumeData = Current.files.contents(atPath: resumeDataPath.string) - return attemptResumableTask(maximumRetryCount: 3) { [weak self] resumeData -> AnyPublisher in + return attemptResumableTask(maximumRetryCount: 3) { resumeData -> AnyPublisher in let (progress, publisher) = Current.network.downloadTask(with: availableXcode.url, to: destination.url, resumingWith: resumeData ?? persistedResumeData) progressChanged(progress) - self?.updateDockIcon(withProgress: progress) + return publisher .map { $0.saveLocation } .eraseToAnyPublisher() @@ -177,13 +182,11 @@ extension AppState { }) .eraseToAnyPublisher() } - - private func updateDockIcon(withProgress progress: Progress) { - DockProgress.style = .bar - DockProgress.progressInstance = progress - } public func installArchivedXcode(_ availableXcode: AvailableXcode, at archiveURL: URL) -> AnyPublisher { + unxipProgress.completedUnitCount = 0 + overallProgress.addChild(unxipProgress, withPendingUnitCount: AppState.unxipProgressWeight) + do { let destinationURL = Path.installDirectory.join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url switch archiveURL.pathExtension { @@ -423,6 +426,9 @@ extension AppState { } self.presentedAlert = .privilegedHelper } + + unxipProgress.completedUnitCount = AppState.totalProgressUnits + resetDockProgressTracking() return helperInstallConsentSubject .flatMap { @@ -463,6 +469,24 @@ extension AppState { .eraseToAnyPublisher() } + // MARK: - Dock Progress Tracking + + private func setupDockProgress() { + DockProgress.progressInstance = nil + DockProgress.style = .bar + + let progress = Progress(totalUnitCount: AppState.totalProgressUnits) + progress.kind = .file + progress.fileOperationKind = .downloading + overallProgress = progress + + DockProgress.progressInstance = overallProgress + } + + func resetDockProgressTracking() { + DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete + } + // MARK: - func setInstallationStep(of version: Version, to step: InstallationStep) { diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index ca629f2..49e22de 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -107,6 +107,19 @@ class AppState: ObservableObject { private var selectPublisher: AnyCancellable? private var uninstallPublisher: AnyCancellable? private var autoInstallTimer: Timer? + + // MARK: - Dock Progress Tracking + + public static let totalProgressUnits = Int64(10) + public static let unxipProgressWeight = Int64(1) + var overallProgress = Progress() + var unxipProgress = { + let progress = Progress(totalUnitCount: totalProgressUnits) + progress.kind = .file + progress.fileOperationKind = .copying + return progress + }() + // MARK: - var dataSource: DataSource { @@ -491,8 +504,7 @@ class AppState: ObservableObject { // Cancel the publisher installationPublishers[id] = nil - // Remove dock icon progress indicator - DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete + resetDockProgressTracking() // If the download is cancelled by the user, clean up the download files that aria2 creates. // This isn't done as part of the publisher with handleEvents(receiveCancel:) because it shouldn't happen when e.g. the app quits. From b6e654c6f22d61c3200576eb4cb726ae2169d91e Mon Sep 17 00:00:00 2001 From: Sam Lu Date: Tue, 19 Sep 2023 12:16:06 -0600 Subject: [PATCH 4/4] Update CI workflow to use Xcode 14.2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a4a580..eb4d569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,5 +13,5 @@ jobs: - uses: actions/checkout@v3 - name: Run tests env: - DEVELOPER_DIR: /Applications/Xcode_13.2.1.app + DEVELOPER_DIR: /Applications/Xcode_14.2.app run: xcodebuild test -scheme Xcodes