Skip to content

Commit d404738

Browse files
authored
Merge pull request #2289 from bnbarham/infinite-root-2
Standardize file paths when attempting to find toolchains
2 parents aa7ff70 + 4e87021 commit d404738

File tree

4 files changed

+97
-29
lines changed

4 files changed

+97
-29
lines changed

Sources/SwiftExtensions/URLExtensions.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ extension URL {
8787
}
8888
}
8989

90+
/// Assuming this URL is a file URL, checks if it looks like a root path. This is a string check, ie. the return
91+
/// value for a path of `"/foo/.."` would be `false`. An error will be thrown is this is a non-file URL.
9092
package var isRoot: Bool {
9193
get throws {
9294
let checkPath = try filePath

Sources/ToolchainRegistry/Toolchain.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,10 +417,23 @@ public final class Toolchain: Sendable {
417417
}
418418

419419
/// Find a containing xctoolchain with plist, if available.
420-
func containingXCToolchain(
420+
private func containingXCToolchain(
421421
_ path: URL
422422
) -> (XCToolchainPlist, URL)? {
423-
var path = path
423+
// `deletingLastPathComponent` only makes sense on resolved paths (ie. those without symlinks or `..`). Any given
424+
// toolchain path should have already been realpathed, but since this can turn into an infinite loop otherwise, it's
425+
// better to be safe than sorry.
426+
let resolvedPath = orLog("Toolchain realpath") {
427+
try path.realpath
428+
}
429+
guard let resolvedPath else {
430+
return nil
431+
}
432+
if path != resolvedPath {
433+
logger.fault("\(path) was not realpathed")
434+
}
435+
436+
var path = resolvedPath
424437
while !((try? path.isRoot) ?? true) {
425438
if path.pathExtension == "xctoolchain" {
426439
if let infoPlist = orLog("Loading information from xctoolchain", { try XCToolchainPlist(fromDirectory: path) }) {

Sources/ToolchainRegistry/ToolchainRegistry.swift

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@ package final actor ToolchainRegistry {
6060
private let toolchainsByIdentifier: [String: [Toolchain]]
6161

6262
/// The toolchains indexed by their path.
63-
private let toolchainsByPath: [URL: Toolchain]
63+
private var toolchainsByPath: [URL: Toolchain]
6464

6565
/// Map from compiler paths (`clang`, `swift`, `swiftc`) mapping to the toolchain that contained them.
6666
///
6767
/// This allows us to find the toolchain that should be used for semantic functionality based on which compiler it is
6868
/// built with in the `compile_commands.json`.
69-
private let toolchainsByCompiler: [URL: Toolchain]
69+
private var toolchainsByCompiler: [URL: Toolchain]
7070

7171
/// The currently selected toolchain identifier on Darwin.
7272
package let darwinToolchainOverride: String?
@@ -96,6 +96,13 @@ package final actor ToolchainRegistry {
9696
var toolchainsByPath: [URL: Toolchain] = [:]
9797
var toolchainsByCompiler: [URL: Toolchain] = [:]
9898
for (toolchain, reason) in toolchainsAndReasonsParam {
99+
// Toolchain should always be unique by path. It isn't particularly useful to log if we already have a toolchain
100+
// though, as we could have just found toolchains through symlinks (this is actually quite normal - eg. OSS
101+
// toolchains add a `swift-latest.xctoolchain` symlink on macOS).
102+
if toolchainsByPath[toolchain.path] != nil {
103+
continue
104+
}
105+
99106
// Non-XcodeDefault toolchain: disallow all duplicates.
100107
if toolchainsByIdentifier[toolchain.identifier] != nil,
101108
toolchain.identifier != ToolchainRegistry.darwinDefaultToolchainIdentifier
@@ -104,12 +111,6 @@ package final actor ToolchainRegistry {
104111
continue
105112
}
106113

107-
// Toolchain should always be unique by path.
108-
if toolchainsByPath[toolchain.path] != nil {
109-
logger.fault("Found two toolchains with the same path: \(toolchain.path)")
110-
continue
111-
}
112-
113114
toolchainsByPath[toolchain.path] = toolchain
114115
toolchainsByIdentifier[toolchain.identifier, default: []].append(toolchain)
115116

@@ -218,9 +219,14 @@ package final actor ToolchainRegistry {
218219
}
219220
}
220221

221-
let toolchainsAndReasons = toolchainPaths.compactMap {
222-
if let toolchain = Toolchain($0.path) {
223-
return (toolchain, $0.reason)
222+
let toolchainsAndReasons = toolchainPaths.compactMap { toolchainAndReason in
223+
let resolvedPath = orLog("Toolchain realpath") {
224+
try toolchainAndReason.path.realpath
225+
}
226+
if let resolvedPath,
227+
let toolchain = Toolchain(resolvedPath)
228+
{
229+
return (toolchain, toolchainAndReason.reason)
224230
}
225231
return nil
226232
}
@@ -283,7 +289,43 @@ package final actor ToolchainRegistry {
283289
/// If we have a toolchain in the toolchain registry that contains the compiler with the given URL, return it.
284290
/// Otherwise, return `nil`.
285291
package func toolchain(withCompiler compiler: URL) -> Toolchain? {
286-
return toolchainsByCompiler[compiler]
292+
if let toolchain = toolchainsByCompiler[compiler] {
293+
return toolchain
294+
}
295+
296+
// Only canonicalize the folder path, as we don't want to resolve symlinks to eg. `swift-driver`.
297+
let resolvedPath = orLog("Compiler realpath") {
298+
try compiler.deletingLastPathComponent().realpath
299+
}?.appending(component: compiler.lastPathComponent)
300+
guard let resolvedPath,
301+
let toolchain = toolchainsByCompiler[resolvedPath]
302+
else {
303+
return nil
304+
}
305+
306+
// Cache mapping of non-realpath to the realpath toolchain for faster subsequent lookups
307+
toolchainsByCompiler[compiler] = toolchain
308+
return toolchain
309+
}
310+
311+
/// If we have a toolchain in the toolchain registry with the given URL, return it. Otherwise, return `nil`.
312+
package func toolchain(withPath path: URL) -> Toolchain? {
313+
if let toolchain = toolchainsByPath[path] {
314+
return toolchain
315+
}
316+
317+
let resolvedPath = orLog("Toolchain realpath") {
318+
try path.realpath
319+
}
320+
guard let resolvedPath,
321+
let toolchain = toolchainsByPath[resolvedPath]
322+
else {
323+
return nil
324+
}
325+
326+
// Cache mapping of non-realpath to the realpath toolchain for faster subsequent lookups
327+
toolchainsByPath[path] = toolchain
328+
return toolchain
287329
}
288330
}
289331

@@ -292,10 +334,6 @@ extension ToolchainRegistry {
292334
package func toolchains(withIdentifier identifier: String) -> [Toolchain] {
293335
return toolchainsByIdentifier[identifier] ?? []
294336
}
295-
296-
package func toolchain(withPath path: URL) -> Toolchain? {
297-
return toolchainsByPath[path]
298-
}
299337
}
300338

301339
extension ToolchainRegistry {

Tests/ToolchainRegistryTests/ToolchainRegistryTests.swift

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,12 @@ final class ToolchainRegistryTests: XCTestCase {
325325

326326
func testSearchPATH() async throws {
327327
try await withTestScratchDir { tempDir in
328-
let binPath = tempDir.appendingPathComponent("bin", isDirectory: true)
329-
try makeToolchain(binPath: binPath, sourcekitd: true)
328+
let binPath = tempDir.appending(component: "bin", directoryHint: .isDirectory)
329+
try makeToolchain(binPath: binPath, clang: true, sourcekitd: true)
330+
331+
let compilerPath = binPath.appending(component: "clang" + (Platform.current?.executableExtension ?? ""))
332+
let linkPath = tempDir.appending(component: "link")
333+
try FileManager.default.createSymbolicLink(at: linkPath, withDestinationURL: compilerPath)
330334

331335
#if os(Windows)
332336
let separator: String = ";"
@@ -336,7 +340,12 @@ final class ToolchainRegistryTests: XCTestCase {
336340

337341
try ProcessEnv.setVar(
338342
"SOURCEKIT_PATH_FOR_TEST",
339-
value: ["/bogus", binPath.filePath, "/bogus2"].joined(separator: separator)
343+
value: [
344+
"/bogus/../parent",
345+
"/bogus",
346+
binPath.appending(components: "..", "bin", directoryHint: .isDirectory).filePath,
347+
"/bogus2",
348+
].joined(separator: separator)
340349
)
341350
defer { try! ProcessEnv.setVar("SOURCEKIT_PATH_FOR_TEST", value: "") }
342351

@@ -349,15 +358,21 @@ final class ToolchainRegistryTests: XCTestCase {
349358
darwinToolchainOverride: nil
350359
)
351360

352-
let tc = try unwrap(await tr.toolchains.first(where: { $0.path == binPath }))
361+
let pathTC = try unwrap(await tr.toolchain(withPath: binPath))
353362

354-
await assertEqual(tr.default?.identifier, tc.identifier)
355-
XCTAssertEqual(tc.identifier, try binPath.filePath)
356-
XCTAssertNil(tc.clang)
357-
XCTAssertNil(tc.clangd)
358-
XCTAssertNil(tc.swiftc)
359-
XCTAssertNotNil(tc.sourcekitd)
360-
XCTAssertNil(tc.libIndexStore)
363+
await assertEqual(tr.default?.identifier, pathTC.identifier)
364+
XCTAssertEqual(pathTC.identifier, try binPath.filePath)
365+
XCTAssertNotNil(pathTC.clang)
366+
XCTAssertNil(pathTC.clangd)
367+
XCTAssertNil(pathTC.swiftc)
368+
XCTAssertNotNil(pathTC.sourcekitd)
369+
XCTAssertNil(pathTC.libIndexStore)
370+
371+
let compilerTC = try unwrap(await tr.toolchain(withCompiler: compilerPath))
372+
XCTAssertEqual(compilerTC.identifier, try binPath.filePath)
373+
374+
let linkTC = await tr.toolchain(withCompiler: linkPath)
375+
XCTAssertNil(linkTC)
361376
}
362377
}
363378

0 commit comments

Comments
 (0)