Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Xcodes/Backend/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,11 @@ class AppState: ObservableObject {
)

self.allXcodes = items.map { item in
Xcode(item, icon: item.installedPath.map { NSWorkspace.shared.icon(forFile: $0.string) })
Xcode(
item,
icon: item.installedPath.map { NSWorkspace.shared.icon(forFile: $0.string) },
installedSDKBuilds: item.installedPath.map { InstalledSDKBuilds.builds(forXcodeAt: $0, version: item.version) } ?? []
)
}
}

Expand Down
58 changes: 55 additions & 3 deletions Xcodes/Backend/Xcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ struct Xcode: Identifiable, CustomStringConvertible {
let compilers: Compilers?
let downloadFileSize: Int64?
let architectures: [Architecture]?
/// SDK build identifiers read from the installed bundle, used when the data source has no SDK metadata
let installedSDKBuilds: [String]
let id: XcodeID

init(
Expand All @@ -34,7 +36,8 @@ struct Xcode: Identifiable, CustomStringConvertible {
sdks: SDKs? = nil,
compilers: Compilers? = nil,
downloadFileSize: Int64? = nil,
architectures: [Architecture]? = nil
architectures: [Architecture]? = nil,
installedSDKBuilds: [String] = []
) {
self.identicalBuilds = identicalBuilds
self.installState = installState
Expand All @@ -47,10 +50,11 @@ struct Xcode: Identifiable, CustomStringConvertible {
self.compilers = compilers
self.downloadFileSize = downloadFileSize
self.architectures = architectures
self.installedSDKBuilds = installedSDKBuilds
self.id = XcodeID(version: version, architectures: architectures)
}

init(_ item: XcodeListItem, icon: NSImage?) {
init(_ item: XcodeListItem, icon: NSImage?, installedSDKBuilds: [String] = []) {
self.identicalBuilds = item.identicalBuilds
self.installState = item.installState
self.selected = item.selected
Expand All @@ -62,6 +66,7 @@ struct Xcode: Identifiable, CustomStringConvertible {
self.compilers = item.compilers
self.downloadFileSize = item.downloadFileSize
self.architectures = item.architectures
self.installedSDKBuilds = installedSDKBuilds
self.id = item.id
}

Expand Down Expand Up @@ -96,5 +101,52 @@ struct Xcode: Identifiable, CustomStringConvertible {
var installedPath: Path? {
installState.installedPath
}


/// SDK builds used to find matching platform runtimes. An installed bundle is the source of truth,
/// since the Apple data source has no SDK metadata and a renamed beta may not match its release.
var platformSDKBuilds: [String] {
installedSDKBuilds.isEmpty ? (sdks?.allBuilds ?? []) : installedSDKBuilds
}
}

enum InstalledSDKBuilds {
private static let lock = NSLock()
nonisolated(unsafe) private static var cache: [String: [String]] = [:]

/// Reads the ProductBuildVersion of each SDK in an installed Xcode, e.g. iPhoneOS.sdk -> 24A5422a.
static func builds(forXcodeAt path: Path, version: Version) -> [String] {
let key = "\(path.string)|\(version.buildMetadataIdentifiers.joined())"
lock.lock()
if let cached = cache[key] {
lock.unlock()
return cached
}
lock.unlock()

let fileManager = FileManager.default
let platformsURL = path.url.appending(path: "Contents/Developer/Platforms")
let platformURLs = (try? fileManager.contentsOfDirectory(at: platformsURL, includingPropertiesForKeys: nil)) ?? []
var builds: [String] = []
for platformURL in platformURLs where platformURL.pathExtension == "platform" {
let sdksURL = platformURL.appending(path: "Developer/SDKs")
let sdkURLs = (try? fileManager.contentsOfDirectory(at: sdksURL, includingPropertiesForKeys: [.isSymbolicLinkKey])) ?? []
for sdkURL in sdkURLs where sdkURL.pathExtension == "sdk" {
// Versioned SDK names (e.g. iPhoneOS27.0.sdk) are symlinks to the same SDK
if (try? sdkURL.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink) == true { continue }
let systemVersionURL = sdkURL.appending(path: "System/Library/CoreServices/SystemVersion.plist")
guard
let data = Current.files.contents(atPath: systemVersionURL.path),
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any],
let build = plist["ProductBuildVersion"] as? String,
!builds.contains(build)
else { continue }
builds.append(build)
}
}

lock.lock()
cache[key] = builds
lock.unlock()
return builds
}
}
6 changes: 3 additions & 3 deletions Xcodes/Frontend/InfoPane/PlatformsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ struct PlatformsView: View {

var body: some View {

let builds = xcode.sdks?.allBuilds
let availableRuntimes = (builds?.flatMap { sdkBuild in
let builds = xcode.platformSDKBuilds
let availableRuntimes = builds.flatMap { sdkBuild in
appState.downloadableRuntimes.filter {
$0.sdkBuildUpdate?.contains(sdkBuild) ?? false
}
} ?? []).removingReleaseCandidateDisplayDuplicates(installedRuntimes: appState.installedRuntimes)
}.removingReleaseCandidateDisplayDuplicates(installedRuntimes: appState.installedRuntimes)

let availableVariants = ArchitectureVariant.allCases.filter { variant in
availableRuntimes.contains { $0.supports(variant) }
Expand Down
10 changes: 7 additions & 3 deletions Xcodes/Frontend/Preferences/PlatformsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,19 @@ struct PlatformsListView: View {
.task {
loadRuntimes()
}
.onChange(of: appState.installedRuntimes) { _ in
.onChange(of: appState.installedRuntimes) {
loadRuntimes()
}
.onChange(of: appState.downloadableRuntimes) {
loadRuntimes()
}
}

func loadRuntimes() {
// Match on architecture too, so a build published as both Apple Silicon and Universal
// only lists the variant that is actually installed (and can therefore be deleted).
let filteredRuntimes = appState.downloadableRuntimes.filter { runtime in
appState.installedRuntimes.contains { $0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate
}
appState.coreSimulatorInfo(runtime: runtime) != nil
}
runtimes = OrderedDictionary(grouping: filteredRuntimes, by: { $0.platform })
}
Expand Down
32 changes: 32 additions & 0 deletions XcodesTests/AppStateUpdateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,38 @@ class AppStateUpdateTests: XCTestCase {
XCTAssertEqual(subject.selectedXcodePath, "/Applications/Xcode-Beta.app")
}

func test_InstalledSDKBuilds_ReadsEachRealSDKOnce() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
let xcodeURL = root.appending(path: "Xcode.app")
func addSDK(platform: String, sdk: String, build: String) throws {
let coreServices = xcodeURL.appending(path: "Contents/Developer/Platforms/\(platform).platform/Developer/SDKs/\(sdk).sdk/System/Library/CoreServices")
try FileManager.default.createDirectory(at: coreServices, withIntermediateDirectories: true)
let plist = try PropertyListSerialization.data(fromPropertyList: ["ProductBuildVersion": build], format: .xml, options: 0)
try plist.write(to: coreServices.appending(path: "SystemVersion.plist"))
}
try addSDK(platform: "iPhoneOS", sdk: "iPhoneOS", build: "24A5422a")
try addSDK(platform: "iPhoneSimulator", sdk: "iPhoneSimulator", build: "24A5422a")
try addSDK(platform: "XROS", sdk: "XROS", build: "24M5357a")
// Versioned SDK names are symlinks to the real SDK and must be skipped
let sdksURL = xcodeURL.appending(path: "Contents/Developer/Platforms/XROS.platform/Developer/SDKs")
try FileManager.default.createSymbolicLink(at: sdksURL.appending(path: "XROS27.0.sdk"), withDestinationURL: sdksURL.appending(path: "XROS.sdk"))
Current.files.contentsAtPath = { FileManager.default.contents(atPath: $0) }

let builds = InstalledSDKBuilds.builds(forXcodeAt: Path(url: xcodeURL)!, version: Version("27.0.0-beta.6+27A5252f")!)

XCTAssertEqual(Set(builds), ["24A5422a", "24M5357a"])
XCTAssertEqual(builds.count, 2)
}

func test_PlatformSDKBuilds_PreferInstalledBundleOverReleaseMetadata() {
let xcode = Xcode(version: Version("27.0.0")!, installState: .notInstalled, selected: false, icon: nil, installedSDKBuilds: ["24A5422a"])
XCTAssertEqual(xcode.platformSDKBuilds, ["24A5422a"])

let notInstalled = Xcode(version: Version("27.0.0")!, installState: .notInstalled, selected: false, icon: nil)
XCTAssertEqual(notInstalled.platformSDKBuilds, [])
}

func testDoesNotReplaceInstallState() throws {
subject.allXcodes = [
Xcode(version: Version("0.0.0")!, installState: .installing(.unarchiving), selected: false, icon: nil)
Expand Down