From 581ba79b527d16419be3258320d7feb134efa375 Mon Sep 17 00:00:00 2001 From: Matt Graham Date: Mon, 15 Sep 2025 23:57:54 +0100 Subject: [PATCH 1/2] Filter a single version for each build --- Sources/XcodesKit/XcodeList.swift | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Sources/XcodesKit/XcodeList.swift b/Sources/XcodesKit/XcodeList.swift index b46c970..a8927fb 100644 --- a/Sources/XcodesKit/XcodeList.swift +++ b/Sources/XcodesKit/XcodeList.swift @@ -173,22 +173,23 @@ extension XcodeList { /// 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(_ xcodes: [Xcode]) -> [Xcode] { + + let xcodesByBuildMetadataIdentifiers = + Dictionary(grouping: xcodes, by: { $0.version.buildMetadataIdentifiers }) + var filteredXcodes: [Xcode] = [] - for xcode in xcodes { - if xcode.version.buildMetadataIdentifiers.isEmpty { - filteredXcodes.append(xcode) + for (buildMetadataIdentifiers, xcodes) in xcodesByBuildMetadataIdentifiers { + if buildMetadataIdentifiers.isEmpty || xcodes.count == 1 { + filteredXcodes.append(contentsOf: xcodes) continue } - let xcodesWithSameBuildMetadataIdentifiers = xcodes - .filter({ $0.version.buildMetadataIdentifiers == xcode.version.buildMetadataIdentifiers }) - if xcodesWithSameBuildMetadataIdentifiers.count > 1, - xcode.version.prereleaseIdentifiers.isEmpty || xcode.version.prereleaseIdentifiers == ["GM"] { - filteredXcodes.append(xcode) - } else if xcodesWithSameBuildMetadataIdentifiers.count == 1 { - filteredXcodes.append(xcode) - } + // Use the final release if there is one, otherwise just (arbitrarily) pick the first. + let finalRelease = xcodes.first(where: { + $0.version.prereleaseIdentifiers.isEmpty || $0.version.prereleaseIdentifiers == ["GM"] }) + filteredXcodes.append(finalRelease ?? xcodes.first!) } + return filteredXcodes } } From 5beff1138da3c2de7f55950415a58e1f28538944 Mon Sep 17 00:00:00 2001 From: Matt Graham Date: Fri, 3 Oct 2025 00:23:38 +0100 Subject: [PATCH 2/2] add tests --- Sources/XcodesKit/XcodeList.swift | 5 +- Tests/XcodesKitTests/XcodeListTests.swift | 61 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 Tests/XcodesKitTests/XcodeListTests.swift diff --git a/Sources/XcodesKit/XcodeList.swift b/Sources/XcodesKit/XcodeList.swift index a8927fb..f3a3ed6 100644 --- a/Sources/XcodesKit/XcodeList.swift +++ b/Sources/XcodesKit/XcodeList.swift @@ -165,14 +165,15 @@ extension XcodeList { } return xcodes } - .map(filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers) + .map(XcodeList.filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers) } /// 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(_ xcodes: [Xcode]) -> [Xcode] { + /// This may not preserve order. + static func filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers(_ xcodes: [Xcode]) -> [Xcode] { let xcodesByBuildMetadataIdentifiers = Dictionary(grouping: xcodes, by: { $0.version.buildMetadataIdentifiers }) diff --git a/Tests/XcodesKitTests/XcodeListTests.swift b/Tests/XcodesKitTests/XcodeListTests.swift new file mode 100644 index 0000000..1176e42 --- /dev/null +++ b/Tests/XcodesKitTests/XcodeListTests.swift @@ -0,0 +1,61 @@ +import XCTest +import Version +@testable import XcodesKit + +class XcodeListTests: XCTestCase { + + func xcodeFromVersion(version: Version?) -> Xcode + { + return Xcode(version: version!, + url: URL(fileURLWithPath: "https://developer.apple.com/Xcode_example.app"), + filename: "Xcode_example.app", + releaseDate: nil) + } + + let versions = [ + + // Single version + Version(major: 1, minor: 1, patch: 1, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["1.1.1.build1"]), + + // 2 versions with the same build, one is beta + Version(major: 1, minor: 2, patch: 1, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["1.2.1.build1"]), + Version(major: 1, minor: 2, patch: 1, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["1.2.1.build1"]), + + // 2 versions with the same build, one is beta (other GM) + Version(major: 1, minor: 3, patch: 1, prereleaseIdentifiers: ["GM"], buildMetadataIdentifiers: ["1.3.1.build1"]), + Version(major: 1, minor: 3, patch: 1, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["1.3.1.build1"]), + + // 2 versions with no buildMetaIdentifiers. + Version(major: 1, minor: 4, patch: 1, prereleaseIdentifiers: [], buildMetadataIdentifiers: []), + Version(major: 1, minor: 4, patch: 2, prereleaseIdentifiers: [], buildMetadataIdentifiers: []), + ] + + let filteredVersionsExpected = [ + // Single version + Version(major: 1, minor: 1, patch: 1, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["1.1.1.build1"]), + + // 2 versions with the same build, one is beta + Version(major: 1, minor: 2, patch: 1, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["1.2.1.build1"]), + //Version(major: 1, minor: 2, patch: 1, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["1.2.1.build1"]), + + // 2 versions with the same build, one is beta (other GM) + Version(major: 1, minor: 3, patch: 1, prereleaseIdentifiers: ["GM"], buildMetadataIdentifiers: ["1.3.1.build1"]), + //Version(major: 1, minor: 3, patch: 1, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["1.3.1.build1"]), + + // 2 versions with no buildMetaIdentifiers. + Version(major: 1, minor: 4, patch: 1, prereleaseIdentifiers: [], buildMetadataIdentifiers: []), + Version(major: 1, minor: 4, patch: 2, prereleaseIdentifiers: [], buildMetadataIdentifiers: []), + ] + + var filteredVersions : [Version] = [] + + override func setUpWithError() throws { + let xcodes = versions.map(xcodeFromVersion) + let filteredXcodes = XcodeList.filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers(xcodes) + filteredVersions = filteredXcodes.map { $0.version } + } + + func test_filterPrereleasesThatMatchReleaseBuildMetadataIdentifiers() { + XCTAssertEqual(filteredVersions.sorted(), filteredVersionsExpected.sorted()) + } +}