Skip to content

Commit 15a8d85

Browse files
committed
Standardize file paths when attempting to find toolchains
`containingXCToolchain` loops infinitely when given eg. `C:\foo\..\bar`. The underlying cause is that `deletingLastPathComponent` does not remove the `..` on Windows. On macOS it's potentially worse - it *adds* `..`. We don't see the infinite loop on macOS/Linux though because `AbsolutePath` already removes them (which is not the case on Windows). Resolves #2174. (cherry picked from commit 4e87021)
1 parent 529cd78 commit 15a8d85

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
@@ -86,6 +86,8 @@ extension URL {
8686
}
8787
}
8888

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

Sources/ToolchainRegistry/Toolchain.swift

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

368368
/// Find a containing xctoolchain with plist, if available.
369-
func containingXCToolchain(
369+
private func containingXCToolchain(
370370
_ path: URL
371371
) -> (XCToolchainPlist, URL)? {
372-
var path = path
372+
// `deletingLastPathComponent` only makes sense on resolved paths (ie. those without symlinks or `..`). Any given
373+
// toolchain path should have already been realpathed, but since this can turn into an infinite loop otherwise, it's
374+
// better to be safe than sorry.
375+
let resolvedPath = orLog("Toolchain realpath") {
376+
try path.realpath
377+
}
378+
guard let resolvedPath else {
379+
return nil
380+
}
381+
if path != resolvedPath {
382+
logger.fault("\(path) was not realpathed")
383+
}
384+
385+
var path = resolvedPath
373386
while !((try? path.isRoot) ?? true) {
374387
if path.pathExtension == "xctoolchain" {
375388
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
}
@@ -285,7 +291,43 @@ package final actor ToolchainRegistry {
285291
/// If we have a toolchain in the toolchain registry that contains the compiler with the given URL, return it.
286292
/// Otherwise, return `nil`.
287293
package func toolchain(withCompiler compiler: URL) -> Toolchain? {
288-
return toolchainsByCompiler[compiler]
294+
if let toolchain = toolchainsByCompiler[compiler] {
295+
return toolchain
296+
}
297+
298+
// Only canonicalize the folder path, as we don't want to resolve symlinks to eg. `swift-driver`.
299+
let resolvedPath = orLog("Compiler realpath") {
300+
try compiler.deletingLastPathComponent().realpath
301+
}?.appending(component: compiler.lastPathComponent)
302+
guard let resolvedPath,
303+
let toolchain = toolchainsByCompiler[resolvedPath]
304+
else {
305+
return nil
306+
}
307+
308+
// Cache mapping of non-realpath to the realpath toolchain for faster subsequent lookups
309+
toolchainsByCompiler[compiler] = toolchain
310+
return toolchain
311+
}
312+
313+
/// If we have a toolchain in the toolchain registry with the given URL, return it. Otherwise, return `nil`.
314+
package func toolchain(withPath path: URL) -> Toolchain? {
315+
if let toolchain = toolchainsByPath[path] {
316+
return toolchain
317+
}
318+
319+
let resolvedPath = orLog("Toolchain realpath") {
320+
try path.realpath
321+
}
322+
guard let resolvedPath,
323+
let toolchain = toolchainsByPath[resolvedPath]
324+
else {
325+
return nil
326+
}
327+
328+
// Cache mapping of non-realpath to the realpath toolchain for faster subsequent lookups
329+
toolchainsByPath[path] = toolchain
330+
return toolchain
289331
}
290332
}
291333

@@ -294,10 +336,6 @@ extension ToolchainRegistry {
294336
package func toolchains(withIdentifier identifier: String) -> [Toolchain] {
295337
return toolchainsByIdentifier[identifier] ?? []
296338
}
297-
298-
package func toolchain(withPath path: URL) -> Toolchain? {
299-
return toolchainsByPath[path]
300-
}
301339
}
302340

303341
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)