From a0f03c8f446678079e81572452f125cf0d18ac54 Mon Sep 17 00:00:00 2001 From: Ugo Mignon <56931733+TartanLeGrand@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:31:19 +0200 Subject: [PATCH 1/4] packer: pass -isysroot to clang when linking swiftc hands clang --sysroot at link time, but clang's Darwin driver only reads SDKSettings.json for -isysroot. The linker then received the deployment target as the SDK version, so LC_BUILD_VERSION.sdk equaled minos (e.g. 17.0 instead of 27.1), which App Store Connect checks. Add -Xclang-linker -isysroot to the linker settings of the app and extension targets when a Darwin SDK is installed. --- Sources/PackLib/DarwinSDK.swift | 19 +++++++++++ Sources/PackLib/Packer.swift | 43 +++++++++++++++++++++--- Tests/XToolTests/PackerTests.swift | 54 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/Sources/PackLib/DarwinSDK.swift b/Sources/PackLib/DarwinSDK.swift index cadd72dd..a3d865f2 100644 --- a/Sources/PackLib/DarwinSDK.swift +++ b/Sources/PackLib/DarwinSDK.swift @@ -41,6 +41,25 @@ public struct DarwinSDK { public let version: String public let flavor: Flavor + /// The SDK directory (e.g. `iPhoneOS27.1.sdk`) that builds for `triple` link against, if installed. + /// + /// The unversioned `iPhoneOS.sdk` symlink is not used because its target is what carries the version. + package func sdkRoot(forTriple triple: String) -> URL? { + let platform: String + switch Packer.cfBundleSupportedPlatforms(for: triple) { + case ["iPhoneSimulator"]: platform = "iPhoneSimulator" + case ["MacOSX"]: platform = "MacOSX" + default: platform = "iPhoneOS" + } + let sdks = bundle.appendingPathComponent("Developer/Platforms/\(platform).platform/Developer/SDKs") + let names = (try? FileManager.default.contentsOfDirectory(atPath: sdks.path)) ?? [] + let versioned = names + .filter { $0.hasPrefix(platform) && $0.hasSuffix(".sdk") && $0 != "\(platform).sdk" } + .sorted { $0.compare($1, options: .numeric) == .orderedAscending } + return (versioned.last ?? names.first { $0 == "\(platform).sdk" }) + .map { sdks.appendingPathComponent($0, isDirectory: true) } + } + static func swiftPMDirectory( environment: [String: String] = ProcessInfo.processInfo.environment, homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser diff --git a/Sources/PackLib/Packer.swift b/Sources/PackLib/Packer.swift index bd2057b7..387688b6 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -11,12 +11,37 @@ public struct Packer: Sendable { self.buildSettings = buildSettings } + /// Linker flags that make clang read the SDK's `SDKSettings.json`. + /// + /// swiftc links through clang but hands it `--sysroot `. Clang's Darwin driver only derives the + /// SDK version from `-isysroot`, so without this the `-platform_version` it gives the linker has + /// the deployment target in the SDK slot, and `LC_BUILD_VERSION.sdk` ends up equal to `minos` + /// (e.g. 17.0 instead of 27.1). App Store Connect checks that field against its minimum SDK. + /// + /// This lives here and not in the Swift SDK's toolset because toolset options cannot be relative + /// to the SDK, whereas the installed SDK's location is known when we write the package, and it + /// also fixes SDKs installed before this was added. + package static func sysrootLinkerFlags(sdkRoot: URL?) -> [String] { + guard let sdkRoot else { return [] } + return ["-Xclang-linker", "-isysroot", "-Xclang-linker", sdkRoot.path] + } + + private func sdkRootForLinking() -> URL? { + #if os(macOS) + // Xcode's toolchain already passes -isysroot. + return nil + #else + return (try? DarwinSDK.current())?.sdkRoot(forTriple: buildSettings.triple) + #endif + } + private func build() async throws { let xtoolDir = URL(fileURLWithPath: "xtool") let packageDir = xtoolDir.appendingPathComponent(".xtool-tmp") try? FileManager.default.removeItem(at: packageDir) try FileManager.default.createDirectory(at: packageDir, withIntermediateDirectories: true) + let sysrootFlags = Packer.sysrootLinkerFlags(sdkRoot: sdkRootForLinking()) let packageSwift = packageDir.appendingPathComponent("Package.swift") let contents = """ // swift-tools-version: 6.0 @@ -38,7 +63,7 @@ public struct Packer: Sendable { dependencies: [ .product(name: "\($0.product)", package: "RootPackage"), ], - linkerSettings: \($0.linkerSettings) + linkerSettings: \($0.linkerSettings(extraFlags: sysrootFlags)) ) """ } @@ -233,16 +258,19 @@ public struct Packer: Sendable { } extension Plan.Product { - fileprivate var linkerSettings: String { + /// Swift source for the `linkerSettings:` argument of the generated target. + package func linkerSettings(extraFlags: [String]) -> String { + let extra = extraFlags.map { "\"\($0.swiftStringEscaped)\"," }.joined(separator: "\n") switch self.type { - case .application: """ + case .application: return """ [ .unsafeFlags([ "-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks", + \(extra) ]), ] """ - case .appExtension: """ + case .appExtension: return """ [ // Link to Foundation framework which implements the _NSExtensionMain entrypoint .linkedFramework("Foundation"), @@ -253,9 +281,16 @@ extension Plan.Product { "-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../Frameworks", // ...as well as our own "-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks", + \(extra) ]), ] """ } } } + +private extension String { + var swiftStringEscaped: String { + replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + } +} diff --git a/Tests/XToolTests/PackerTests.swift b/Tests/XToolTests/PackerTests.swift index 5aabda83..599d154e 100644 --- a/Tests/XToolTests/PackerTests.swift +++ b/Tests/XToolTests/PackerTests.swift @@ -1,3 +1,4 @@ +import Foundation import Testing @testable import PackLib @@ -13,3 +14,56 @@ import Testing #expect(Packer.cfBundleSupportedPlatforms(for: "unknown-triple") == ["iPhoneOS"]) } + +private func makeProduct(type: Plan.ProductType) -> Plan.Product { + Plan.Product( + type: type, + product: "Demo", + deploymentTarget: "17.0", + bundleID: "com.example.demo", + infoPlist: [:], + resources: [] + ) +} + +@Test func sysrootLinkerFlagsPassIsysrootToClang() { + let sdk = URL(fileURLWithPath: "/sdks/iPhoneOS27.1.sdk") + #expect(Packer.sysrootLinkerFlags(sdkRoot: sdk) == [ + "-Xclang-linker", "-isysroot", "-Xclang-linker", "/sdks/iPhoneOS27.1.sdk", + ]) + #expect(Packer.sysrootLinkerFlags(sdkRoot: nil).isEmpty) +} + +@Test(arguments: [Plan.ProductType.application, .appExtension]) +func generatedLinkerSettingsIncludeSysroot(type: Plan.ProductType) { + let flags = Packer.sysrootLinkerFlags(sdkRoot: URL(fileURLWithPath: "/sdks/with \"quote\"/iPhoneOS27.1.sdk")) + let settings = makeProduct(type: type).linkerSettings(extraFlags: flags) + #expect(settings.contains("\"-Xclang-linker\",")) + #expect(settings.contains("\"-isysroot\",")) + #expect(settings.contains(#""/sdks/with \"quote\"/iPhoneOS27.1.sdk","#)) + #expect(settings.contains("@executable_path/Frameworks")) + + #expect(!makeProduct(type: type).linkerSettings(extraFlags: []).contains("isysroot")) +} + +@Test func sdkRootFollowsTheTripleAndPicksVersionedSDK() throws { + let bundle = FileManager.default.temporaryDirectory + .appendingPathComponent("SysrootTests-\(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: bundle) } + let sdks = [ + "iPhoneOS.platform/Developer/SDKs/iPhoneOS27.1.sdk", + "iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator27.1.sdk", + ] + for sdk in sdks { + try FileManager.default.createDirectory( + at: bundle.appendingPathComponent("Developer/Platforms/\(sdk)"), + withIntermediateDirectories: true + ) + } + try Data("epoch=4\n".utf8).write(to: bundle.appendingPathComponent("darwin-sdk-version.txt")) + let sdk = try #require(DarwinSDK(bundle: bundle)) + + #expect(sdk.sdkRoot(forTriple: "arm64-apple-ios")?.lastPathComponent == "iPhoneOS27.1.sdk") + #expect(sdk.sdkRoot(forTriple: "arm64-apple-ios17.0-simulator")?.lastPathComponent == "iPhoneSimulator27.1.sdk") + #expect(sdk.sdkRoot(forTriple: "arm64-apple-macosx") == nil) +} From 88a417d0592017f84732a0cfc1fa921195507190 Mon Sep 17 00:00:00 2001 From: Ugo Mignon <56931733+TartanLeGrand@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:34:54 +0200 Subject: [PATCH 2/4] packer: link against the SDK swift-sdk.json names Use the sdkRootPath that swift-sdk.json records for the target triple, so the link uses the same SDK as the compilation when several versioned SDKs are installed. Bundles without the file, and triples it does not list, keep using the newest versioned SDK on disk. --- Sources/PackLib/DarwinSDK.swift | 34 ++++++++++++++++++++++++++++- Tests/XToolTests/PackerTests.swift | 35 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/Sources/PackLib/DarwinSDK.swift b/Sources/PackLib/DarwinSDK.swift index a3d865f2..7ccfa3b2 100644 --- a/Sources/PackLib/DarwinSDK.swift +++ b/Sources/PackLib/DarwinSDK.swift @@ -43,8 +43,40 @@ public struct DarwinSDK { /// The SDK directory (e.g. `iPhoneOS27.1.sdk`) that builds for `triple` link against, if installed. /// - /// The unversioned `iPhoneOS.sdk` symlink is not used because its target is what carries the version. + /// This is the SDK `swift-sdk.json` names for the triple, i.e. the one SwiftPM compiles against, so + /// the link uses the same SDK as the compilation. Bundles from before that file listed it fall back + /// to the newest versioned SDK on disk. package func sdkRoot(forTriple triple: String) -> URL? { + sdkRootFromMetadata(forTriple: triple) ?? newestSDKRoot(forTriple: triple) + } + + private func sdkRootFromMetadata(forTriple triple: String) -> URL? { + struct Definition: Decodable { + struct Target: Decodable { + let sdkRootPath: String + } + let targetTriples: [String: Target] + } + guard let data = try? Data(contentsOf: bundle.appendingPathComponent("swift-sdk.json")), + let definition = try? JSONDecoder().decode(Definition.self, from: data) + else { return nil } + + // `triple` may carry an OS version (arm64-apple-ios17.0-simulator) that the keys don't + let wanted = Self.versionless(triple) + let target = definition.targetTriples.first { Self.versionless($0.key) == wanted }?.value + return target + .map { bundle.appendingPathComponent($0.sdkRootPath, isDirectory: true) } + .flatMap { $0.dirExists ? $0 : nil } + } + + /// `arm64-apple-ios17.0-simulator` -> `arm64-apple-ios-simulator` + private static func versionless(_ triple: String) -> String { + triple.split(separator: "-", omittingEmptySubsequences: false) + .map { $0.prefix { !$0.isNumber } } + .joined(separator: "-") + } + + private func newestSDKRoot(forTriple triple: String) -> URL? { let platform: String switch Packer.cfBundleSupportedPlatforms(for: triple) { case ["iPhoneSimulator"]: platform = "iPhoneSimulator" diff --git a/Tests/XToolTests/PackerTests.swift b/Tests/XToolTests/PackerTests.swift index 599d154e..c0208526 100644 --- a/Tests/XToolTests/PackerTests.swift +++ b/Tests/XToolTests/PackerTests.swift @@ -67,3 +67,38 @@ func generatedLinkerSettingsIncludeSysroot(type: Plan.ProductType) { #expect(sdk.sdkRoot(forTriple: "arm64-apple-ios17.0-simulator")?.lastPathComponent == "iPhoneSimulator27.1.sdk") #expect(sdk.sdkRoot(forTriple: "arm64-apple-macosx") == nil) } + +@Test func sdkRootFollowsSwiftSDKMetadata() throws { + let bundle = FileManager.default.temporaryDirectory + .appendingPathComponent("SysrootMetadataTests-\(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: bundle) } + for sdk in [ + "iPhoneOS.platform/Developer/SDKs/iPhoneOS26.5.sdk", + "iPhoneOS.platform/Developer/SDKs/iPhoneOS27.1.sdk", + "iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.5.sdk", + "iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator27.1.sdk", + ] { + try FileManager.default.createDirectory( + at: bundle.appendingPathComponent("Developer/Platforms/\(sdk)"), + withIntermediateDirectories: true + ) + } + try Data("epoch=4\n".utf8).write(to: bundle.appendingPathComponent("darwin-sdk-version.txt")) + // SDKBuilder records the first matching SDK, which is not the newest one here + let metadata = """ + {"schemaVersion": "4.0", "targetTriples": { + "arm64-apple-ios": {"sdkRootPath": "Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.5.sdk"}, + "arm64-apple-ios-simulator": { + "sdkRootPath": "Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.5.sdk" + } + }} + """ + try Data(metadata.utf8).write(to: bundle.appendingPathComponent("swift-sdk.json")) + let sdk = try #require(DarwinSDK(bundle: bundle)) + + #expect(sdk.sdkRoot(forTriple: "arm64-apple-ios")?.lastPathComponent == "iPhoneOS26.5.sdk") + #expect(sdk.sdkRoot(forTriple: "arm64-apple-ios17.0")?.lastPathComponent == "iPhoneOS26.5.sdk") + #expect(sdk.sdkRoot(forTriple: "arm64-apple-ios17.0-simulator")?.lastPathComponent == "iPhoneSimulator26.5.sdk") + // a triple the metadata doesn't list falls back to the newest SDK on disk + #expect(sdk.sdkRoot(forTriple: "x86_64-apple-ios-simulator")?.lastPathComponent == "iPhoneSimulator27.1.sdk") +} From 442f7359ae68970c62974c10c3cfafdbacac270b Mon Sep 17 00:00:00 2001 From: Ugo Mignon <56931733+TartanLeGrand@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:25:34 +0200 Subject: [PATCH 3/4] packer: warn when swift-sdk.json does not resolve an SDK When swift-sdk.json is present but has no entry for the triple, or names an SDK directory that is not on disk, the linker falls back to the newest versioned SDK, which may differ from the one SwiftPM compiles against. Keep the fallback but say so on stderr. --- Sources/PackLib/DarwinSDK.swift | 55 ++++++++++++++++----- Sources/PackLib/Packer.swift | 1 + Tests/XToolTests/DarwinSDKTests.swift | 71 +++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 11 deletions(-) diff --git a/Sources/PackLib/DarwinSDK.swift b/Sources/PackLib/DarwinSDK.swift index 7ccfa3b2..2fb9f884 100644 --- a/Sources/PackLib/DarwinSDK.swift +++ b/Sources/PackLib/DarwinSDK.swift @@ -45,28 +45,61 @@ public struct DarwinSDK { /// /// This is the SDK `swift-sdk.json` names for the triple, i.e. the one SwiftPM compiles against, so /// the link uses the same SDK as the compilation. Bundles from before that file listed it fall back - /// to the newest versioned SDK on disk. - package func sdkRoot(forTriple triple: String) -> URL? { - sdkRootFromMetadata(forTriple: triple) ?? newestSDKRoot(forTriple: triple) + /// to the newest versioned SDK on disk. When the file is there but does not lead to an SDK on disk, + /// the fallback may differ from what SwiftPM compiles against, so `warn` is told about it. + package func sdkRoot( + forTriple triple: String, + warn: (String) -> Void = { stderrPrint($0) } + ) -> URL? { + let metadata = sdkRootFromMetadata(forTriple: triple) + if case .found(let url) = metadata { return url } + let fallback = newestSDKRoot(forTriple: triple) + let file = bundle.appendingPathComponent("swift-sdk.json").path + let reason: String + switch metadata { + case .found, .absent: + return fallback + case .unreadable: + reason = "'\(file)' could not be read" + case .noEntry: + reason = "'\(file)' has no entry for \(triple)" + case .missing(let url): + reason = "'\(file)' names an SDK for \(triple) that is not installed at '\(url.path)'" + } + let used = fallback.map { "'\($0.path)'" } ?? "none found" + warn("warning: \(reason); linking against a different SDK than SwiftPM compiles against (\(used))") + return fallback + } + + package enum MetadataSDKRoot: Equatable { + /// No `swift-sdk.json`, as in bundles from before it listed the SDK paths. + case absent + case unreadable + case noEntry + case missing(URL) + case found(URL) } - private func sdkRootFromMetadata(forTriple triple: String) -> URL? { + package func sdkRootFromMetadata(forTriple triple: String) -> MetadataSDKRoot { struct Definition: Decodable { struct Target: Decodable { let sdkRootPath: String } let targetTriples: [String: Target] } - guard let data = try? Data(contentsOf: bundle.appendingPathComponent("swift-sdk.json")), - let definition = try? JSONDecoder().decode(Definition.self, from: data) - else { return nil } + guard let data = try? Data(contentsOf: bundle.appendingPathComponent("swift-sdk.json")) else { + return .absent + } + guard let definition = try? JSONDecoder().decode(Definition.self, from: data) else { + return .unreadable + } // `triple` may carry an OS version (arm64-apple-ios17.0-simulator) that the keys don't let wanted = Self.versionless(triple) - let target = definition.targetTriples.first { Self.versionless($0.key) == wanted }?.value - return target - .map { bundle.appendingPathComponent($0.sdkRootPath, isDirectory: true) } - .flatMap { $0.dirExists ? $0 : nil } + guard let target = definition.targetTriples.first(where: { Self.versionless($0.key) == wanted })?.value + else { return .noEntry } + let url = bundle.appendingPathComponent(target.sdkRootPath, isDirectory: true) + return url.dirExists ? .found(url) : .missing(url) } /// `arm64-apple-ios17.0-simulator` -> `arm64-apple-ios-simulator` diff --git a/Sources/PackLib/Packer.swift b/Sources/PackLib/Packer.swift index 387688b6..9edecb9d 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -23,6 +23,7 @@ public struct Packer: Sendable { /// also fixes SDKs installed before this was added. package static func sysrootLinkerFlags(sdkRoot: URL?) -> [String] { guard let sdkRoot else { return [] } + // -Xclang-linker is a Swift driver flag: this only works because targets are linked through swiftc. return ["-Xclang-linker", "-isysroot", "-Xclang-linker", sdkRoot.path] } diff --git a/Tests/XToolTests/DarwinSDKTests.swift b/Tests/XToolTests/DarwinSDKTests.swift index ed5ecb57..e887ecb7 100644 --- a/Tests/XToolTests/DarwinSDKTests.swift +++ b/Tests/XToolTests/DarwinSDKTests.swift @@ -53,3 +53,74 @@ import Testing #expect(!FileManager.default.fileExists(atPath: temporaryURL.path)) #expect(FileManager.default.fileExists(atPath: installedBundle.path)) } + +private func makeSDKBundle(swiftSDKJSON: String?, sdks: [String]) throws -> (URL, DarwinSDK) { + let bundle = FileManager.default.temporaryDirectory + .appendingPathComponent("DarwinSDKTests-\(UUID().uuidString)", isDirectory: true) + .appendingPathComponent("darwin.artifactbundle", isDirectory: true) + try FileManager.default.createDirectory(at: bundle, withIntermediateDirectories: true) + if let swiftSDKJSON { + try Data(swiftSDKJSON.utf8).write(to: bundle.appendingPathComponent("swift-sdk.json")) + } + for sdk in sdks { + try FileManager.default.createDirectory( + at: bundle.appendingPathComponent("Developer/Platforms/iPhoneOS.platform/Developer/SDKs/\(sdk)"), + withIntermediateDirectories: true + ) + } + return (bundle.deletingLastPathComponent(), try #require(DarwinSDK(bundle: bundle))) +} + +private let sdkJSON = """ +{"targetTriples": {"arm64-apple-ios": {"sdkRootPath": "Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS27.0.sdk"}}} +""" + +@Test func sdkRootPrefersSwiftSDKMetadataWithoutWarning() throws { + let (root, sdk) = try makeSDKBundle(swiftSDKJSON: sdkJSON, sdks: ["iPhoneOS27.0.sdk", "iPhoneOS27.1.sdk"]) + defer { try? FileManager.default.removeItem(at: root) } + + var warnings: [String] = [] + let result = sdk.sdkRoot(forTriple: "arm64-apple-ios17.0") { warnings.append($0) } + + #expect(result?.lastPathComponent == "iPhoneOS27.0.sdk") + #expect(warnings.isEmpty) +} + +@Test func sdkRootWarnsWhenMetadataSDKIsNotOnDisk() throws { + let (root, sdk) = try makeSDKBundle(swiftSDKJSON: sdkJSON, sdks: ["iPhoneOS26.0.sdk", "iPhoneOS27.1.sdk"]) + defer { try? FileManager.default.removeItem(at: root) } + + var warnings: [String] = [] + let result = sdk.sdkRoot(forTriple: "arm64-apple-ios17.0") { warnings.append($0) } + + #expect(result?.lastPathComponent == "iPhoneOS27.1.sdk") + #expect(warnings.count == 1) + #expect(warnings.first?.contains("swift-sdk.json") == true) + #expect(warnings.first?.contains("iPhoneOS27.0.sdk") == true) + #expect(warnings.first?.contains("iPhoneOS27.1.sdk") == true) +} + +@Test func sdkRootWarnsWhenMetadataHasNoEntryForTriple() throws { + let (root, sdk) = try makeSDKBundle(swiftSDKJSON: sdkJSON, sdks: ["iPhoneOS27.1.sdk"]) + defer { try? FileManager.default.removeItem(at: root) } + + var warnings: [String] = [] + let result = sdk.sdkRoot(forTriple: "arm64-apple-ios17.0-simulator") { warnings.append($0) } + + // only an iPhoneOS SDK is installed, so the simulator fallback finds nothing either + #expect(result == nil) + #expect(warnings.count == 1) + #expect(warnings.first?.contains("no entry") == true) + #expect(warnings.first?.contains("none found") == true) +} + +@Test func sdkRootFallsBackSilentlyWithoutMetadata() throws { + let (root, sdk) = try makeSDKBundle(swiftSDKJSON: nil, sdks: ["iPhoneOS27.0.sdk", "iPhoneOS27.1.sdk"]) + defer { try? FileManager.default.removeItem(at: root) } + + var warnings: [String] = [] + let result = sdk.sdkRoot(forTriple: "arm64-apple-ios17.0") { warnings.append($0) } + + #expect(result?.lastPathComponent == "iPhoneOS27.1.sdk") + #expect(warnings.isEmpty) +} From 9ca78402e308fd585b96fa26a82663e75525a71a Mon Sep 17 00:00:00 2001 From: Ugo Mignon <56931733+TartanLeGrand@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:57:32 +0200 Subject: [PATCH 4/4] packer: warn when swift-sdk.json cannot be read A swift-sdk.json that exists but cannot be read was reported as absent, so the fallback stayed silent. Report it as unreadable instead. --- Sources/PackLib/DarwinSDK.swift | 6 +++--- Tests/XToolTests/DarwinSDKTests.swift | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Sources/PackLib/DarwinSDK.swift b/Sources/PackLib/DarwinSDK.swift index 2fb9f884..cb01abce 100644 --- a/Sources/PackLib/DarwinSDK.swift +++ b/Sources/PackLib/DarwinSDK.swift @@ -87,9 +87,9 @@ public struct DarwinSDK { } let targetTriples: [String: Target] } - guard let data = try? Data(contentsOf: bundle.appendingPathComponent("swift-sdk.json")) else { - return .absent - } + let file = bundle.appendingPathComponent("swift-sdk.json") + guard FileManager.default.fileExists(atPath: file.path) else { return .absent } + guard let data = try? Data(contentsOf: file) else { return .unreadable } guard let definition = try? JSONDecoder().decode(Definition.self, from: data) else { return .unreadable } diff --git a/Tests/XToolTests/DarwinSDKTests.swift b/Tests/XToolTests/DarwinSDKTests.swift index e887ecb7..1a6b8cd1 100644 --- a/Tests/XToolTests/DarwinSDKTests.swift +++ b/Tests/XToolTests/DarwinSDKTests.swift @@ -124,3 +124,20 @@ private let sdkJSON = """ #expect(result?.lastPathComponent == "iPhoneOS27.1.sdk") #expect(warnings.isEmpty) } + +@Test func sdkRootWarnsWhenMetadataCannotBeRead() throws { + let (root, sdk) = try makeSDKBundle(swiftSDKJSON: nil, sdks: ["iPhoneOS27.1.sdk"]) + defer { try? FileManager.default.removeItem(at: root) } + // present but not readable as a file + try FileManager.default.createDirectory( + at: sdk.bundle.appendingPathComponent("swift-sdk.json"), + withIntermediateDirectories: false + ) + + var warnings: [String] = [] + let result = sdk.sdkRoot(forTriple: "arm64-apple-ios17.0") { warnings.append($0) } + + #expect(result?.lastPathComponent == "iPhoneOS27.1.sdk") + #expect(warnings.count == 1) + #expect(warnings.first?.contains("could not be read") == true) +}