Skip to content
Merged
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
22 changes: 19 additions & 3 deletions Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3224,21 +3224,37 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
if payload.previewStyle == .xojit {
// Also pass the auxiliary Swift files.
commandLine.append(contentsOf: originalInputs.map(\.str))
selectedInputPath = sourceFile

// Fixes rdar://176386125. The package PIF builder may symlink-resolve source paths in
// the build description (eg, /tmp -> /private/tmp) so they match what the index service stores.
// The previews client, however, asks us to thunk the *unresolved* path, so a plain `sourceFile` won't
// match the command's inputs and libSwiftDriver returns no command line -> `noPreviewInfos`.
//
// Use the spelling that actually appears in the command's inputs, matched by realpath,
// and key the output-file-map / VFS overlay on it. No-op when the paths already agree
// (eg, the legacy package PIF builder available in Xcode).
//
// See the related `previewXOJITThunkInfoResolvesSymlinkedSourcePath` test.
func resolvedPath(_ path: Path) -> Path { (try? fs.realpath(path)) ?? path }
let realSourceFile = resolvedPath(sourceFile)
selectedInputPath = originalInputs.only { resolvedPath($0) == realSourceFile } ?? sourceFile

if let driverPayload = payload.driverPayload {
do {
// Inject the thunk source into the output file map
let pchPath = driverPayload.tempDirPath.join(driverPayload.outputPrefix + "-primary-Bridging-header.pch")
let map = SwiftOutputFileMap(files: [sourceFile.str: .init(object: outputPath.str), "": .init(pch: pchPath.str)])
let map = SwiftOutputFileMap(files: [
selectedInputPath.str: SwiftOutputFileMap.Entry(object: outputPath.str),
"": SwiftOutputFileMap.Entry(pch: pchPath.str)
])
let newOutputFileMap = driverPayload.tempDirPath.join(UUID().uuidString)
try fs.createDirectory(newOutputFileMap.dirname, recursive: true)
try fs.write(newOutputFileMap, contents: ByteString(JSONEncoder(outputFormatting: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]).encode(map)))
commandLine.append(contentsOf: ["-output-file-map", newOutputFileMap.str])

// rdar://127735418 ([JIT] Emit a vfsoverlay for JIT preview thunk compiler arguments so clients can specify the original file path when substituting contents)
let vfs = VFS()
vfs.addMapping(sourceFile, externalContents: inputPath)
vfs.addMapping(selectedInputPath, externalContents: inputPath)
newVFSOverlayPath = driverPayload.tempDirPath.join("vfsoverlay-\(inputPath.basename).json")
try fs.createDirectory(newOutputFileMap.dirname, recursive: true)
let overlay = try vfs.toVFSOverlay().propertyListItem.asJSONFragment().asString
Expand Down
19 changes: 18 additions & 1 deletion Sources/SWBUtil/Collection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,28 @@ public extension Collection {
}

/// Return the only element in the collection, if it has exactly one element.
/// - complexity: O(1).
///
/// **Complexity**. O(1).
var only: Iterator.Element? {
return !isEmpty && index(after: startIndex) == endIndex ? self.first : nil
}

/// Returns the single element of the sequence satisfying `predicate`,
/// or `nil`if no element — or more than one element — satisfies it.
///
/// Unlike `first(where:)`, multiple matches yield `nil`;
/// iteration stops as soon as a second match is found.
///
/// **Complexity**. O(n), where n is the length of the sequence.
func only(where predicate: (Element) throws -> Bool) rethrows -> Element? {
var onlyMatch: Element?
for candidate in self where try predicate(candidate) {
guard onlyMatch == nil else { return nil }
onlyMatch = candidate
}
return onlyMatch
}

/// Returns the elements of the sequence, sorted using the given key path as the comparison between elements.
@inlinable func sorted<Value: Comparable>(by predicate: (Element) -> Value) -> [Element] {
return sorted(<, by: predicate)
Expand Down
107 changes: 107 additions & 0 deletions Tests/SWBBuildSystemTests/PreviewsBuildOperationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,113 @@ fileprivate struct PreviewsBuildOperationTests: CoreBasedTests {
}
}

/// Regression test for rdar://176386125: `generatePreviewInfo(.thunkInfo)` must match the
/// requested source file against the compile command's inputs by *resolved* path, not by unresolved path.
///
/// For standalone Swift packages, the OSS (ie, new) *package PIF builder* bakes the
/// symlink-resolved source path (eg, `/private/tmp/foo/main.swift`) into the compile
/// command so it matches what the *index service* stores, while the previews client requests a thunk
/// for the unresolved path (eg, `/tmp/foo/main.swift`). The two paths resolve to the same file but differ,
/// so the broken code — which compared them literally — found no matching input, libSwiftDriver
/// produced no command line, and the request came back empty, which clients report as `noPreviewInfos`.
///
/// We reproduce that path mismatch without a real package by introducing a single symlink `srcRootSymlink`.
/// Before the fix this returns zero preview infos; after the fix it returns one, and the resulting
/// compile command line refers to the build's spelling (`srcRoot`), never the request's symlink.
@Test(.requireSDKs(.iOS))
func previewXOJITThunkInfoResolvesSymlinkedSourcePath() async throws {
try await withTemporaryDirectory { (tmpDirPath: Path) in
// The real source directory. The build references `main.swift` through it, so
// this is the spelling that ends up baked into the compile command's inputs.
let srcRoot = tmpDirPath.join("srcroot")

// A symlink that points at `srcRoot`.
// The preview request references `main.swift` through it.
let srcRootSymlink = tmpDirPath.join("srcroot-symlink")

let sourceFile = TestFile("main.swift")
let testProject = TestProject(
"ProjectName",
sourceRoot: srcRoot,
groupTree: TestGroup(
"Sources", path: "Sources",
children: [sourceFile]
),
buildConfigurations: [
TestBuildConfiguration("Debug", buildSettings: [
"GENERATE_INFOPLIST_FILE": "YES",
"PRODUCT_NAME": "$(TARGET_NAME)",
"SDKROOT": "iphoneos",
"SWIFT_VERSION": "5.0",
"SWIFT_OPTIMIZATION_LEVEL": "-Onone"
])
],
targets: [
TestStandardTarget(
"AppTarget",
type: .application,
buildPhases: [
TestSourcesBuildPhase([TestBuildFile(sourceFile.name)])
]
)
]
)

let core = try await getCore()
let tester = try await BuildOperationTester(core, testProject, simulated: false)

try tester.fs.createDirectory(srcRoot.join("Sources"), recursive: true)
try tester.fs.write(srcRoot.join("Sources").join(sourceFile.name), contents: "")

// The build references the source through `srcRoot`; the preview request references it
// through `srcRootSymlink`, a symlink to the same directory. Same file, different spelling.
try tester.fs.symlink(srcRootSymlink, target: srcRoot)
let buildSourceFile = srcRoot.join("Sources").join(sourceFile.name)
let requestedSourceFile = srcRootSymlink.join("Sources").join(sourceFile.name)

// Sanity check the setup: the two spellings differ as strings but resolve to the same file.
try #require(requestedSourceFile != buildSourceFile)
try #require(try tester.fs.realpath(requestedSourceFile) == tester.fs.realpath(buildSourceFile))

let buildParameters = BuildParameters(
configuration: "Debug",
overrides: ["ENABLE_XOJIT_PREVIEWS": "YES"]
)

try await tester.checkBuild(
parameters: buildParameters,
runDestination: .iOSSimulator,
buildCommand: .build(style: .buildOnly, skipDependencies: false)
) { results in
results.checkNoErrors()

let buildDescription = results.buildDescription
let appTarget = try #require(buildDescription.allConfiguredTargets.first { $0.target.name == "AppTarget" })

let previewInfoInput = TaskGeneratePreviewInfoInput.thunkInfo(
sourceFile: requestedSourceFile,
thunkVariantSuffix: "selection"
)
let previewInfos = buildDescription.generatePreviewInfoForTesting(
for: [appTarget],
workspaceContext: tester.workspaceContext,
buildRequestContext: results.buildRequestContext,
input: previewInfoInput
)

// The crux: before the fix this was empty (-> `noPreviewInfos`), because the requested
// symlinked path didn't match the resolved path baked into the compile command.
#expect(previewInfos.count == 1)
let compileCommandLine: [String] = try #require(previewInfos.only?.thunkInfo?.compileCommandLine)

// And the command we hand back references the spelling that actually appears in the
// build (`srcRoot`), not the symlinked path the client happened to ask with.
#expect(compileCommandLine.contains(buildSourceFile.str))
#expect(!compileCommandLine.contains(requestedSourceFile.str))
}
}
}

private enum LinkStyle {
case dylib
case bundleLoader
Expand Down
Loading