diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 17e4c3a..6ef2ff2 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -44,7 +44,7 @@ extension AppState { } .handleEvents(receiveOutput: { installedXcode in DispatchQueue.main.async { - guard let index = self.allXcodes.firstIndex(where: { $0.version == installedXcode.version || $0.version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) }) else { return } + guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: installedXcode.version) }) else { return } self.allXcodes[index].installState = .installed } }) @@ -54,7 +54,7 @@ extension AppState { private func getXcodeArchive(_ installationType: InstallationType, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> { switch installationType { case .version(let availableXcode): - if let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: availableXcode.version) }) { + if let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.isEquivalent(to: availableXcode.version) }) { return Fail(error: InstallationError.versionAlreadyInstalled(installedXcode)) .eraseToAnyPublisher() } @@ -344,7 +344,7 @@ extension AppState { func setInstallationStep(of version: Version, to step: InstallationStep) { DispatchQueue.main.async { - guard let index = self.allXcodes.firstIndex(where: { $0.version.buildMetadataIdentifiers == version.buildMetadataIdentifiers || $0.version.isEquivalentForDeterminingIfInstalled(toInstalled: version) }) else { return } + guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return } self.allXcodes[index].installState = .installing(step) } } diff --git a/Xcodes/Backend/AppState+Update.swift b/Xcodes/Backend/AppState+Update.swift index 9400bc7..48d9c21 100644 --- a/Xcodes/Backend/AppState+Update.swift +++ b/Xcodes/Backend/AppState+Update.swift @@ -70,7 +70,7 @@ extension AppState { // /download/more doesn't include build numbers, so we trust that if the version number and prerelease identifiers are the same that they're the same build. // If an Xcode version is listed on both sites then prefer the one on /download because the build metadata is used to compare against installed Xcodes. let xcodes = releasedXcodes.filter { releasedXcode in - prereleaseXcodes.contains { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: releasedXcode.version) } == false + prereleaseXcodes.contains { $0.version.isEquivalent(to: releasedXcode.version) } == false } + prereleaseXcodes return xcodes } diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index e665d55..1b88ea1 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -369,14 +369,14 @@ class AppState: ObservableObject { // Xcode Releases should have all versions // Apple didn't used to keep all prerelease versions around but has started to recently else if !allAvailableXcodeVersions.contains(where: { version in - version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) + version.isEquivalent(to: installedXcode.version) }) { allAvailableXcodeVersions.append(installedXcode.version) } // If an installed version is the same as one that's listed online which doesn't have build metadata, replace it with the installed version // This was originally added for Apple versions else if let index = allAvailableXcodeVersions.firstIndex(where: { version in - version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) && + version.isEquivalent(to: installedXcode.version) && version.buildMetadataIdentifiers.isEmpty }) { allAvailableXcodeVersions[index] = installedXcode.version @@ -388,10 +388,7 @@ class AppState: ObservableObject { .sorted(by: { $0.0 > $1.0 }) .map { availableXcodeVersion, availableXcode in let installedXcode = installedXcodes.first(where: { installedXcode in - // Checking equality for Xcode Releases version - availableXcodeVersion == installedXcode.version || - // Check more carefully for Apple version - availableXcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) + availableXcodeVersion.isEquivalent(to: installedXcode.version) }) // If the existing install state is "installing", keep it diff --git a/Xcodes/Backend/Version+.swift b/Xcodes/Backend/Version+.swift index 5fed036..e86d4a5 100644 --- a/Xcodes/Backend/Version+.swift +++ b/Xcodes/Backend/Version+.swift @@ -1,41 +1,24 @@ import Version public extension Version { - func isEqualWithoutBuildMetadataIdentifiers(to other: Version) -> Bool { - return major == other.major && - minor == other.minor && - patch == other.patch && - prereleaseIdentifiers == other.prereleaseIdentifiers - } - - /// If release versions, don't compare build metadata because that's not provided in the /downloads/more list - /// if beta versions, compare build metadata because it's available in versions.plist - func isEquivalentForDeterminingIfInstalled(toInstalled installed: Version) -> Bool { - let isBeta = !prereleaseIdentifiers.isEmpty - let otherIsBeta = !installed.prereleaseIdentifiers.isEmpty - - if isBeta && otherIsBeta { - if buildMetadataIdentifiers.isEmpty { - return major == installed.major && - minor == installed.minor && - patch == installed.patch && - prereleaseIdentifiers.map { $0.lowercased() } == installed.prereleaseIdentifiers.map { $0.lowercased() } - } - else { - return major == installed.major && - minor == installed.minor && - patch == installed.patch && - prereleaseIdentifiers.map { $0.lowercased() } == installed.prereleaseIdentifiers.map { $0.lowercased() } && - buildMetadataIdentifiers.map { $0.lowercased() } == installed.buildMetadataIdentifiers.map { $0.lowercased() } - } + /// Determines if two Xcode versions should be treated equivalently. This is not the same as equality. + /// + /// We need a way to determine if two Xcode versions are the same without always having full information, and supporting different data sources. + /// For example, the Apple data source often doesn't have build metadata identifiers. + func isEquivalent(to other: Version) -> Bool { + // If we don't have build metadata identifiers for both Versions, compare major, minor, patch and prerelease identifiers. + if buildMetadataIdentifiers.isEmpty || other.buildMetadataIdentifiers.isEmpty { + return major == other.major && + minor == other.minor && + patch == other.patch && + prereleaseIdentifiers.map { $0.lowercased() } == other.prereleaseIdentifiers.map { $0.lowercased() } + // If we have build metadata identifiers for both, we can ignore the prerelease identifiers. + } else { + return major == other.major && + minor == other.minor && + patch == other.patch && + buildMetadataIdentifiers.map { $0.lowercased() } == other.buildMetadataIdentifiers.map { $0.lowercased() } } - else if !isBeta && !otherIsBeta { - return major == installed.major && - minor == installed.minor && - patch == installed.patch - } - - return false } var descriptionWithoutBuildMetadata: String {