Only adjust available versions for Apple data source

This commit is contained in:
Brandon Evans 2021-02-04 19:12:52 -07:00
parent 7ac4814420
commit bc45daeb74
No known key found for this signature in database
GPG key ID: D58A4B8DB64F8E93
3 changed files with 148 additions and 44 deletions

View file

@ -6,10 +6,6 @@ import SwiftSoup
import struct XCModel.Xcode
extension AppState {
private var dataSource: DataSource {
Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default
}
func updateIfNeeded() {
guard
let lastUpdated = Current.defaults.date(forKey: "lastUpdated"),
@ -200,31 +196,6 @@ extension AppState {
}
return xcodes
}
.map(filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers)
.eraseToAnyPublisher()
}
/// Xcode Releases may have multiple releases with the same build metadata when a build doesn't change between candidate and final releases.
/// For example, 12.3 RC and 12.3 are both build 12C33
/// We don't care about that difference, so only keep the final release (GM or Release, in XCModel terms).
/// The downside of this is that a user could technically have both releases installed, and so they won't both be shown in the list, but I think most users wouldn't do this.
func filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers(_ availableXcodes: [AvailableXcode]) -> [AvailableXcode] {
var filteredAvailableXcodes: [AvailableXcode] = []
for availableXcode in availableXcodes {
if availableXcode.version.buildMetadataIdentifiers.isEmpty {
filteredAvailableXcodes.append(availableXcode)
continue
}
let availableXcodesWithSameBuildMetadataIdentifiers = availableXcodes
.filter({ $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers })
if availableXcodesWithSameBuildMetadataIdentifiers.count > 1,
availableXcode.version.prereleaseIdentifiers.isEmpty || availableXcode.version.prereleaseIdentifiers == ["GM"] {
filteredAvailableXcodes.append(availableXcode)
} else if availableXcodesWithSameBuildMetadataIdentifiers.count == 1 {
filteredAvailableXcodes.append(availableXcode)
}
}
return filteredAvailableXcodes
}
}

View file

@ -57,6 +57,12 @@ class AppState: ObservableObject {
private var selectPublisher: AnyCancellable?
private var uninstallPublisher: AnyCancellable?
// MARK: -
var dataSource: DataSource {
Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default
}
// MARK: - Init
init() {
@ -419,23 +425,25 @@ class AppState: ObservableObject {
}
func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) {
var adjustedAvailableXcodes = availableXcodes
var adjustedAvailableXcodes = filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers(availableXcodes)
// First, adjust all of the available Xcodes so that available and installed versions line up and the second part of this function works properly.
for installedXcode in installedXcodes {
// We can trust that build metadata identifiers are unique for each version of Xcode, so if we have it then it's all we need.
// If build metadata matches exactly, replace the available version with the installed version.
// This should handle both Xcode Releases versions which can have different prerelease identifiers and Apple versions which rarely have build metadata identifiers.
if let index = adjustedAvailableXcodes.map(\.version).firstIndex(where: { $0.buildMetadataIdentifiers == installedXcode.version.buildMetadataIdentifiers }) {
adjustedAvailableXcodes[index].version = 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
// Not all prerelease Apple versions available online include build metadata
else if let index = adjustedAvailableXcodes.firstIndex(where: { availableXcode in
availableXcode.version.isEquivalent(to: installedXcode.version) &&
availableXcode.version.buildMetadataIdentifiers.isEmpty
}) {
adjustedAvailableXcodes[index].version = installedXcode.version
if dataSource == .apple {
for installedXcode in installedXcodes {
// We can trust that build metadata identifiers are unique for each version of Xcode, so if we have it then it's all we need.
// If build metadata matches exactly, replace the available version with the installed version.
// This should handle Apple versions from /downloads/more which don't have build metadata identifiers.
if let index = adjustedAvailableXcodes.map(\.version).firstIndex(where: { $0.buildMetadataIdentifiers == installedXcode.version.buildMetadataIdentifiers }) {
adjustedAvailableXcodes[index].version = 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
// Not all prerelease Apple versions available online include build metadata
else if let index = adjustedAvailableXcodes.firstIndex(where: { availableXcode in
availableXcode.version.isEquivalent(to: installedXcode.version) &&
availableXcode.version.buildMetadataIdentifiers.isEmpty
}) {
adjustedAvailableXcodes[index].version = installedXcode.version
}
}
}
@ -453,6 +461,7 @@ class AppState: ObservableObject {
return Xcode(
version: availableXcode.version,
identicalBuilds: [],
installState: existingXcodeInstallState ?? defaultXcodeInstallState,
selected: installedXcode != nil && selectedXcodePath?.hasPrefix(installedXcode!.path.string) == true,
icon: (installedXcode?.path.string).map(NSWorkspace.shared.icon(forFile:)),
@ -483,6 +492,30 @@ class AppState: ObservableObject {
self.allXcodes = newAllXcodes.sorted { $0.version > $1.version }
}
/// Xcode Releases may have multiple releases with the same build metadata when a build doesn't change between candidate and final releases.
/// For example, 12.3 RC and 12.3 are both build 12C33
/// We don't care about that difference, so only keep the final release (GM or Release, in XCModel terms).
/// The downside of this is that a user could technically have both releases installed, and so they won't both be shown in the list, but I think most users wouldn't do this.
func filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers(_ availableXcodes: [AvailableXcode]) -> [AvailableXcode] {
var filteredAvailableXcodes: [AvailableXcode] = []
for availableXcode in availableXcodes {
if availableXcode.version.buildMetadataIdentifiers.isEmpty {
filteredAvailableXcodes.append(availableXcode)
continue
}
let availableXcodesWithSameBuildMetadataIdentifiers = availableXcodes
.filter({ $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers })
if availableXcodesWithSameBuildMetadataIdentifiers.count > 1,
availableXcode.version.prereleaseIdentifiers.isEmpty || availableXcode.version.prereleaseIdentifiers == ["GM"] {
filteredAvailableXcodes.append(availableXcode)
} else if availableXcodesWithSameBuildMetadataIdentifiers.count == 1 {
filteredAvailableXcodes.append(availableXcode)
}
}
return filteredAvailableXcodes
}
// MARK: - Private
private func uninstallXcode(path: Path) -> AnyPublisher<Void, Error> {

View file

@ -46,6 +46,14 @@ class AppStateUpdateTests: XCTestCase {
}
func testDeterminesIfInstalledByBuildMetadataAlone() throws {
Current.defaults.string = { key in
if key == "dataSource" {
return "apple"
} else {
return nil
}
}
subject.allXcodes = [
]
@ -66,6 +74,14 @@ class AppStateUpdateTests: XCTestCase {
}
func testAdjustedVersionsAreUsedToLookupAvailableXcode() throws {
Current.defaults.string = { key in
if key == "dataSource" {
return "apple"
} else {
return nil
}
}
subject.allXcodes = [
]
@ -105,6 +121,90 @@ class AppStateUpdateTests: XCTestCase {
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("1.2.3")!, Version("0.0.0+ABC123")!])
}
func testIdenticalBuilds_KeepsReleaseVersion_WithNeitherInstalled() {
Current.defaults.string = { key in
if key == "dataSource" {
return "xcodeReleases"
} else {
return nil
}
}
subject.allXcodes = [
]
subject.updateAllXcodes(
availableXcodes: [
AvailableXcode(version: Version("12.4.0+12D4e")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil),
AvailableXcode(version: Version("12.4.0-RC+12D4e")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil),
],
installedXcodes: [
],
selectedXcodePath: nil
)
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0+12D4e")!])
}
func testIdenticalBuilds_KeepsReleaseVersion_WithPrereleaseInstalled() {
Current.defaults.string = { key in
if key == "dataSource" {
return "xcodeReleases"
} else {
return nil
}
}
subject.allXcodes = [
]
Current.files.contentsAtPath = { path in
if path.contains("Info.plist") {
return """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.apple.dt.Xcode</string>
<key>CFBundleShortVersionString</key>
<string>12.4.0</string>
</dict>
</plist>
""".data(using: .utf8)
}
else if path.contains("version.plist") {
return """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ProductBuildVersion</key>
<string>12D4e</string>
</dict>
</plist>
""".data(using: .utf8)
}
else {
return nil
}
}
subject.updateAllXcodes(
availableXcodes: [
AvailableXcode(version: Version("12.4.0+12D4e")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil),
AvailableXcode(version: Version("12.4.0-RC+12D4e")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil),
],
installedXcodes: [
InstalledXcode(path: Path("/Applications/Xcode-12.4.0-RC.app")!)!
],
selectedXcodePath: nil
)
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0+12D4e")!])
}
func testFilterReleasesThatMatchPrereleases() {
let result = subject.filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers(
[