From a8797e3751155af7a06200b41ac4c5161566fb98 Mon Sep 17 00:00:00 2001 From: Jason Prasad Date: Tue, 21 Jul 2026 13:23:55 -0400 Subject: [PATCH] core: capture the host-arch slice of fat Xcode builds (R03 iOS crash) A generic iOS Simulator destination builds every ARCHS slice, so the build log carries one swift-frontend invocation per arch and the capture could return the x86_64 one. The JIT then materialized x86_64 code in the arm64 agent, which died with SIGILL executing an x86_64 prologue. Two layers: XcodeCommandCapture.parse prefers the invocation whose -target matches the arch the agent runs as (log order no longer decides), and stripForeignTargetTriple treats a foreign-arch triple as foreign so Compiler's injected target wins for stale persisted captures. Verified: unit rows on both layers; R03 iOS renders again end-to-end, first with the stale x86_64 persisted capture (strip layer) and then with a fresh capture that now persists arm64 (parse layer). Co-Authored-By: Claude Fable 5 --- .../PreviewsCore/XcodeBuildSystem.swift | 17 +-- .../PreviewsCore/XcodeCommandCapture.swift | 48 ++++--- .../CompileCaptureTests.swift | 119 ++++++++++++++++++ 3 files changed, 162 insertions(+), 22 deletions(-) diff --git a/previewsmcp/PreviewsCore/XcodeBuildSystem.swift b/previewsmcp/PreviewsCore/XcodeBuildSystem.swift index 6cb54b69..a099113a 100644 --- a/previewsmcp/PreviewsCore/XcodeBuildSystem.swift +++ b/previewsmcp/PreviewsCore/XcodeBuildSystem.swift @@ -371,21 +371,24 @@ public actor XcodeBuildSystem: BuildSystem { /// Xcode pre-processing for the shared normalizer: a scheme can build a /// platform variant the preview environment cannot host (Mac Catalyst - /// under the macOS agent), and the captured `-target`/`-sdk` would - /// otherwise win over Compiler's injection. Keep the captured pair only - /// when the triple matches the preview platform family. + /// under the macOS agent, a foreign-arch slice of a fat build), and the + /// captured `-target`/`-sdk` would otherwise win over Compiler's + /// injection. Keep the captured pair only when the triple matches the + /// preview platform family on the arch the JIT agent runs as. static func stripForeignTargetTriple( - _ args: [String], platform: PreviewPlatform + _ args: [String], platform: PreviewPlatform, + hostArch: String = XcodeBuildSystem.hostArch ) -> [String] { guard let index = args.firstIndex(of: "-target"), index + 1 < args.count else { return args } let triple = args[index + 1] - let compatible = + let family = switch platform { case .macOS: triple.contains("apple-macos") case .iOS: triple.contains("simulator") } + let compatible = family && triple.hasPrefix("\(hostArch)-") guard !compatible else { return args } var result = args result.removeSubrange(index ... index + 1) @@ -583,11 +586,11 @@ public actor XcodeBuildSystem: BuildSystem { /// Collect source files from the OutputFileMap.json produced by xcodebuild. /// Returns nil if the file doesn't exist (falls back to Tier 1). func collectSourceFiles(settings: [String: String], targetName: String) -> [URL]? { - // OutputFileMap lives at /arm64/-OutputFileMap.json + // OutputFileMap lives at //-OutputFileMap.json guard let objectFileDir = settings["OBJECT_FILE_DIR_normal"] else { return nil } let outputFileMapPath = URL(fileURLWithPath: objectFileDir) - .appendingPathComponent("arm64") + .appendingPathComponent(Self.hostArch) .appendingPathComponent("\(targetName)-OutputFileMap.json") guard let data = try? Data(contentsOf: outputFileMapPath), diff --git a/previewsmcp/PreviewsCore/XcodeCommandCapture.swift b/previewsmcp/PreviewsCore/XcodeCommandCapture.swift index e8ceb87d..fe6c350b 100644 --- a/previewsmcp/PreviewsCore/XcodeCommandCapture.swift +++ b/previewsmcp/PreviewsCore/XcodeCommandCapture.swift @@ -23,28 +23,48 @@ enum XcodeCommandCapture { /// Parse a build log for the module's compile command. Nil when the log /// has no matching SwiftDriver invocation (null build, or a build system - /// that does not log one). The target's C/ObjC objects are deliberately + /// that does not log one). A generic-destination build compiles every + /// ARCHS slice and logs one invocation per arch; only the host-arch + /// slice produces code the JIT agent can execute, so that invocation + /// wins over log order. The target's C/ObjC objects are deliberately /// not read from the log — an incremental build only logs CompileC for /// changed sources — they come from the objects directory on disk. - static func parse(log: String, moduleName: String) -> CapturedCommand? { + static func parse( + log: String, moduleName: String, + hostArch: String = XcodeBuildSystem.hostArch + ) -> CapturedCommand? { + var fallback: [String]? for rawLine in log.split(separator: "\n", omittingEmptySubsequences: true) { let line = rawLine.trimmingCharacters(in: .whitespaces) guard let marker = driverMarkers.first(where: line.contains) else { continue } let argvText = String(line[line.range(of: marker)!.upperBound...]) let tokens = tokenizeShellEscaped(argvText) guard tokens.count > 1, moduleTokenMatches(tokens, moduleName) else { continue } - var args: [String] = [] - var swiftSources: [String] = [] - for token in tokens.dropFirst() { - if token.hasPrefix("@"), token.hasSuffix(".SwiftFileList") { - swiftSources = responseFileLines(String(token.dropFirst())) - } else { - args.append(token) - } + if value(after: "-target", in: tokens)?.hasPrefix("\(hostArch)-") ?? true { + return capture(fromTokens: tokens) } - return CapturedCommand(arguments: args, swiftSources: swiftSources) + if fallback == nil { fallback = tokens } } - return nil + return fallback.map(capture(fromTokens:)) + } + + private static func capture(fromTokens tokens: [String]) -> CapturedCommand { + var args: [String] = [] + var swiftSources: [String] = [] + for token in tokens.dropFirst() { + if token.hasPrefix("@"), token.hasSuffix(".SwiftFileList") { + swiftSources = responseFileLines(String(token.dropFirst())) + } else { + args.append(token) + } + } + return CapturedCommand(arguments: args, swiftSources: swiftSources) + } + + private static func value(after flag: String, in tokens: [String]) -> String? { + guard let index = tokens.firstIndex(of: flag), index + 1 < tokens.count + else { return nil } + return tokens[index + 1] } /// True when the log came from a build system that logs SwiftDriver @@ -159,9 +179,7 @@ enum XcodeCommandCapture { } private static func moduleTokenMatches(_ tokens: [String], _ moduleName: String) -> Bool { - guard let index = tokens.firstIndex(of: "-module-name"), index + 1 < tokens.count - else { return false } - return tokens[index + 1] == moduleName + value(after: "-module-name", in: tokens) == moduleName } private static func responseFileLines(_ path: String) -> [String] { diff --git a/previewsmcp/Tests/PreviewsCoreTests/CompileCaptureTests.swift b/previewsmcp/Tests/PreviewsCoreTests/CompileCaptureTests.swift index 243c5a00..2f82f295 100644 --- a/previewsmcp/Tests/PreviewsCoreTests/CompileCaptureTests.swift +++ b/previewsmcp/Tests/PreviewsCoreTests/CompileCaptureTests.swift @@ -294,6 +294,48 @@ struct XcodeCommandCaptureTests { #expect(!XcodeCommandCapture.logsDriverInvocations("SwiftCompile bazel-out/x")) } + @Test("a fat build's log yields the host-arch driver invocation, either order") + func fatBuildPrefersHostArch() throws { + let log = """ + builtin-SwiftDriver -- /t/swiftc -module-name App -target x86_64-apple-ios26.3-simulator -DDEBUG + builtin-SwiftDriver -- /t/swiftc -module-name App -target arm64-apple-ios26.3-simulator -DDEBUG + """ + let captured = try #require( + XcodeCommandCapture.parse(log: log, moduleName: "App", hostArch: "arm64") + ) + #expect(captured.arguments.contains("arm64-apple-ios26.3-simulator")) + #expect(!captured.arguments.contains("x86_64-apple-ios26.3-simulator")) + let intel = try #require( + XcodeCommandCapture.parse(log: log, moduleName: "App", hostArch: "x86_64") + ) + #expect(intel.arguments.contains("x86_64-apple-ios26.3-simulator")) + } + + @Test("a foreign-arch-only log still captures the first match") + func foreignArchOnlyFallsBack() throws { + let log = """ + builtin-SwiftDriver -- /t/swiftc -module-name App -target x86_64-apple-ios26.3-simulator -DFIRST + builtin-SwiftDriver -- /t/swiftc -module-name App -target x86_64-apple-ios26.3-simulator -DSECOND + """ + let captured = try #require( + XcodeCommandCapture.parse(log: log, moduleName: "App", hostArch: "arm64") + ) + #expect(captured.arguments.contains("-DFIRST")) + #expect(!captured.arguments.contains("-DSECOND")) + } + + @Test("the default hostArch is the arch the agent runs as") + func defaultHostArchWiring() throws { + let host = XcodeBuildSystem.hostArch + let foreign = host == "arm64" ? "x86_64" : "arm64" + let log = """ + builtin-SwiftDriver -- /t/swiftc -module-name App -target \(foreign)-apple-ios26.3-simulator + builtin-SwiftDriver -- /t/swiftc -module-name App -target \(host)-apple-ios26.3-simulator + """ + let captured = try #require(XcodeCommandCapture.parse(log: log, moduleName: "App")) + #expect(captured.arguments.contains("\(host)-apple-ios26.3-simulator")) + } + @Test("escaped spaces in paths survive tokenizing") func escapedSpaces() { let tokens = XcodeCommandCapture.tokenizeShellEscaped( @@ -356,6 +398,83 @@ struct XcodeCommandCaptureTests { } } +@Suite("XcodeBuildSystem.stripForeignTargetTriple") +struct StripForeignTargetTripleTests { + private func args(triple: String) -> [String] { + ["-module-name", "App", "-target", triple, "-sdk", "/sdk/iPhoneSimulator", "-DDEBUG"] + } + + @Test("host-arch triples for the preview platform family are kept") + func hostTriplesKept() { + let simArgs = args(triple: "arm64-apple-ios26.3-simulator") + #expect( + XcodeBuildSystem.stripForeignTargetTriple( + simArgs, platform: .iOS, hostArch: "arm64" + ) == simArgs + ) + let macArgs = args(triple: "arm64-apple-macos26.0") + #expect( + XcodeBuildSystem.stripForeignTargetTriple( + macArgs, platform: .macOS, hostArch: "arm64" + ) == macArgs + ) + } + + @Test("a foreign-arch simulator triple is stripped with its -sdk") + func foreignArchStripped() { + let stripped = XcodeBuildSystem.stripForeignTargetTriple( + args(triple: "x86_64-apple-ios26.3-simulator"), platform: .iOS, hostArch: "arm64" + ) + #expect(stripped == ["-module-name", "App", "-DDEBUG"]) + } + + @Test("a foreign-arch macOS triple is stripped") + func foreignArchMacStripped() { + let stripped = XcodeBuildSystem.stripForeignTargetTriple( + args(triple: "x86_64-apple-macos26.0"), platform: .macOS, hostArch: "arm64" + ) + #expect(stripped == ["-module-name", "App", "-DDEBUG"]) + } + + @Test("a foreign-family triple (Catalyst under iOS) is stripped") + func foreignFamilyStripped() { + let stripped = XcodeBuildSystem.stripForeignTargetTriple( + args(triple: "arm64-apple-ios26.3-macabi"), platform: .iOS, hostArch: "arm64" + ) + #expect(stripped == ["-module-name", "App", "-DDEBUG"]) + } + + @Test("args without -target pass through unchanged, -sdk retained") + func noTargetPassthrough() { + let input = ["-module-name", "App", "-sdk", "/sdk/iPhoneSimulator", "-DDEBUG"] + #expect( + XcodeBuildSystem.stripForeignTargetTriple( + input, platform: .iOS, hostArch: "arm64" + ) == input + ) + } + + @Test("a foreign triple with -sdk before -target strips both pairs") + func sdkBeforeTargetStripped() { + let stripped = XcodeBuildSystem.stripForeignTargetTriple( + [ + "-module-name", "App", "-sdk", "/sdk/iPhoneSimulator", + "-target", "x86_64-apple-ios26.3-simulator", "-DDEBUG", + ], + platform: .iOS, hostArch: "arm64" + ) + #expect(stripped == ["-module-name", "App", "-DDEBUG"]) + } + + @Test("the default hostArch keeps the host triple for the platform family") + func defaultHostArchKept() { + let hostArgs = args(triple: "\(XcodeBuildSystem.hostArch)-apple-ios26.3-simulator") + #expect( + XcodeBuildSystem.stripForeignTargetTriple(hostArgs, platform: .iOS) == hostArgs + ) + } +} + @Suite("BazelCommandCapture") struct BazelCommandCaptureTests { private let jsonProto = """