Fix: Install Apple Silicon shows Universal as Installing

When clicking 'Install Apple Silicon', the UI incorrectly shows the Universal
version as 'Installing' instead of the Apple Silicon version.

Root cause: setInstallationStep used version.isEquivalent() which doesn't
consider architectures. When both Universal and Apple Silicon exist with the
same version, the first one found (Universal) was marked as installing.

Fix: Change setInstallationStep to accept XcodeID instead of Version, enabling
exact matching including architectures.

Fixes #776, #777, #782
This commit is contained in:
William Laverty 2026-02-02 04:05:55 -08:00
parent 1a0d3353b9
commit 701b3397a1
2 changed files with 9 additions and 9 deletions

View file

@ -104,7 +104,7 @@ extension AppState {
private func downloadXcode(availableXcode: AvailableXcode, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> { private func downloadXcode(availableXcode: AvailableXcode, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
DispatchQueue.main.async { DispatchQueue.main.async {
self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress)) self.setInstallationStep(of: availableXcode.xcodeID, to: .downloading(progress: progress))
self.overallProgress.addChild(progress, withPendingUnitCount: AppState.totalProgressUnits - AppState.unxipProgressWeight) self.overallProgress.addChild(progress, withPendingUnitCount: AppState.totalProgressUnits - AppState.unxipProgressWeight)
} }
}) })
@ -203,9 +203,9 @@ extension AppState {
} }
.flatMap { installedXcode -> AnyPublisher<InstalledXcode, Error> in .flatMap { installedXcode -> AnyPublisher<InstalledXcode, Error> in
do { do {
self.setInstallationStep(of: availableXcode.version, to: .trashingArchive) self.setInstallationStep(of: availableXcode.xcodeID, to: .trashingArchive)
try Current.files.trashItem(at: archiveURL) try Current.files.trashItem(at: archiveURL)
self.setInstallationStep(of: availableXcode.version, to: .checkingSecurity) self.setInstallationStep(of: availableXcode.xcodeID, to: .checkingSecurity)
return self.verifySecurityAssessment(of: installedXcode) return self.verifySecurityAssessment(of: installedXcode)
.combineLatest(self.verifySigningCertificate(of: installedXcode.path.url)) .combineLatest(self.verifySigningCertificate(of: installedXcode.path.url))
@ -217,7 +217,7 @@ extension AppState {
} }
} }
.flatMap { installedXcode -> AnyPublisher<InstalledXcode, Error> in .flatMap { installedXcode -> AnyPublisher<InstalledXcode, Error> in
self.setInstallationStep(of: availableXcode.version, to: .finishing) self.setInstallationStep(of: availableXcode.xcodeID, to: .finishing)
return self.performPostInstallSteps(for: installedXcode) return self.performPostInstallSteps(for: installedXcode)
.map { installedXcode } .map { installedXcode }
@ -249,7 +249,7 @@ extension AppState {
} }
func unarchiveAndMoveXIP(availableXcode: AvailableXcode, at source: URL, to destination: URL) -> AnyPublisher<URL, Swift.Error> { func unarchiveAndMoveXIP(availableXcode: AvailableXcode, at source: URL, to destination: URL) -> AnyPublisher<URL, Swift.Error> {
self.setInstallationStep(of: availableXcode.version, to: .unarchiving) self.setInstallationStep(of: availableXcode.xcodeID, to: .unarchiving)
return unxipOrUnxipExperiment(source) return unxipOrUnxipExperiment(source)
.catch { error -> AnyPublisher<ProcessOutput, Swift.Error> in .catch { error -> AnyPublisher<ProcessOutput, Swift.Error> in
@ -267,7 +267,7 @@ extension AppState {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
.tryMap { output -> URL in .tryMap { output -> URL in
self.setInstallationStep(of: availableXcode.version, to: .moving(destination: destination.path)) self.setInstallationStep(of: availableXcode.xcodeID, to: .moving(destination: destination.path))
let xcodeURL = source.deletingLastPathComponent().appendingPathComponent("Xcode.app") let xcodeURL = source.deletingLastPathComponent().appendingPathComponent("Xcode.app")
let xcodeBetaURL = source.deletingLastPathComponent().appendingPathComponent("Xcode-beta.app") let xcodeBetaURL = source.deletingLastPathComponent().appendingPathComponent("Xcode-beta.app")
@ -496,9 +496,9 @@ extension AppState {
// MARK: - // MARK: -
func setInstallationStep(of version: Version, to step: XcodeInstallationStep) { func setInstallationStep(of xcodeID: XcodeID, to step: XcodeInstallationStep) {
DispatchQueue.main.async { DispatchQueue.main.async {
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return } guard let index = self.allXcodes.firstIndex(where: { $0.id == xcodeID }) else { return }
self.allXcodes[index].installState = .installing(step) self.allXcodes[index].installState = .installing(step)
let xcode = self.allXcodes[index] let xcode = self.allXcodes[index]

View file

@ -557,7 +557,7 @@ class AppState: ObservableObject {
installationPublishers[id] = signInIfNeeded() installationPublishers[id] = signInIfNeeded()
.handleEvents( .handleEvents(
receiveSubscription: { [unowned self] _ in receiveSubscription: { [unowned self] _ in
self.setInstallationStep(of: availableXcode.version, to: .authenticating) self.setInstallationStep(of: availableXcode.xcodeID, to: .authenticating)
} }
) )
.flatMap { [unowned self] in .flatMap { [unowned self] in