Skip to content

Commit 29d65a4

Browse files
committed
Resolve /usr/bin/* shims on macOS
CMake was previously doing this itself before 4.0, but seems to be inserting `/usr/bin/*` now. Resolve the `/usr/bin` trampoline ourselves in a similar fashion to swiftly (but with xcrun). Resolves rdar://163462990.
1 parent b539999 commit 29d65a4

File tree

4 files changed

+90
-27
lines changed

4 files changed

+90
-27
lines changed

Sources/BuildServerIntegration/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ add_library(BuildServerIntegration STATIC
2020
LegacyBuildServer.swift
2121
MainFilesProvider.swift
2222
SplitShellCommand.swift
23-
SwiftlyResolver.swift
24-
SwiftPMBuildServer.swift)
23+
SwiftPMBuildServer.swift
24+
SwiftToolchainResolver.swift)
2525
set_target_properties(BuildServerIntegration PROPERTIES
2626
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
2727
target_link_libraries(BuildServerIntegration PUBLIC

Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,20 @@ fileprivate extension CompilationDatabaseCompileCommand {
2626
///
2727
/// The absence of a compiler means we have an empty command line, which should never happen.
2828
///
29-
/// If the compiler is a symlink to `swiftly`, it uses `swiftlyResolver` to find the corresponding executable in a
30-
/// real toolchain and returns that executable.
31-
func compiler(swiftlyResolver: SwiftlyResolver, compileCommandsDirectory: URL) async -> String? {
29+
/// If the compiler is a symlink to `swiftly` or in `/usr/bin` on macOS, it uses `toolchainResolver` to find the
30+
/// corresponding executable in a real toolchain and returns that executable.
31+
func compiler(toolchainResolver: SwiftToolchainResolver, compileCommandsDirectory: URL) async -> String? {
3232
guard let compiler = commandLine.first else {
3333
return nil
3434
}
35-
let swiftlyResolved = await orLog("Resolving swiftly") {
36-
try await swiftlyResolver.resolve(
35+
let resolved = await orLog("Resolving compiler") {
36+
try await toolchainResolver.resolve(
3737
compiler: URL(fileURLWithPath: compiler),
3838
workingDirectory: directoryURL(compileCommandsDirectory: compileCommandsDirectory)
3939
)?.filePath
4040
}
41-
if let swiftlyResolved {
42-
return swiftlyResolved
41+
if let resolved {
42+
return resolved
4343
}
4444
return compiler
4545
}
@@ -74,7 +74,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
7474
/// finds the compilation database in a build directory.
7575
private var configDirectory: URL
7676

77-
private let swiftlyResolver = SwiftlyResolver()
77+
private let toolchainResolver = SwiftToolchainResolver()
7878

7979
// Watch for all all changes to `compile_commands.json` and `compile_flags.txt` instead of just the one at
8080
// `configPath` so that we cover the following semi-common scenario:
@@ -124,7 +124,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
124124
package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse {
125125
let compilers = Set(
126126
await compdb.commands.asyncCompactMap { (command) -> String? in
127-
await command.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
127+
await command.compiler(toolchainResolver: toolchainResolver, compileCommandsDirectory: configDirectory)
128128
}
129129
).sorted { $0 < $1 }
130130
let targets = try await compilers.asyncMap { compiler in
@@ -155,7 +155,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
155155
}
156156
let commandsWithRequestedCompilers = await compdb.commands.lazy.asyncFilter { command in
157157
return await targetCompiler
158-
== command.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
158+
== command.compiler(toolchainResolver: toolchainResolver, compileCommandsDirectory: configDirectory)
159159
}
160160
let sources = commandsWithRequestedCompilers.map {
161161
SourceItem(uri: $0.uri(compileCommandsDirectory: configDirectory), kind: .file, generated: false)
@@ -171,7 +171,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
171171
self.reloadCompilationDatabase()
172172
}
173173
if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == ".swift-version" }) {
174-
await swiftlyResolver.clearCache()
174+
await toolchainResolver.clearCache()
175175
connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil))
176176
}
177177
}
@@ -185,7 +185,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
185185
) async throws -> TextDocumentSourceKitOptionsResponse? {
186186
let targetCompiler = try request.target.compileCommandsCompiler
187187
let command = await compdb[request.textDocument.uri].asyncFilter {
188-
return await $0.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
188+
return await $0.compiler(toolchainResolver: toolchainResolver, compileCommandsDirectory: configDirectory)
189189
== targetCompiler
190190
}.first
191191
guard let command else {

Sources/BuildServerIntegration/SwiftlyResolver.swift renamed to Sources/BuildServerIntegration/SwiftToolchainResolver.swift

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,56 +18,82 @@ import TSCExtensions
1818
import struct TSCBasic.AbsolutePath
1919
import class TSCBasic.Process
2020

21-
/// Given a path to a compiler, which might be a symlink to `swiftly`, this type determines the compiler executable in
22-
/// an actual toolchain. It also caches the results. The client needs to invalidate the cache if the path that swiftly
23-
/// might resolve to has changed, eg. because `.swift-version` has been updated.
24-
actor SwiftlyResolver {
21+
/// Given a path to a compiler, which might be a symlink to `swiftly` or `/usr/bin` on macOS, this type determines the
22+
/// compiler executable in an actual toolchain and caches the result. The client needs to invalidate the cache if the
23+
/// path that this may resolve to has changed, eg. because `.swift-version` or `SDKROOT` has been updated.
24+
actor SwiftToolchainResolver {
2525
private struct CacheKey: Hashable {
2626
let compiler: URL
2727
let workingDirectory: URL?
2828
}
2929

3030
private var cache: LRUCache<CacheKey, Result<URL?, Error>> = LRUCache(capacity: 100)
3131

32-
/// Check if `compiler` is a symlink to `swiftly`. If so, find the executable in the toolchain that swiftly resolves
33-
/// to within the given working directory and return the URL of the corresponding compiler in that toolchain.
34-
/// If `compiler` does not resolve to `swiftly`, return `nil`.
32+
/// Check if `compiler` is a symlink to `swiftly` or in `/usr/bin` on macOS. If so, find the executable in the
33+
/// toolchain that would be resolved to within the given working directory and return the URL of the corresponding
34+
/// compiler in that toolchain. If `compiler` does not resolve to `swiftly` or `/usr/bin` on macOS, return `nil`.
3535
func resolve(compiler: URL, workingDirectory: URL?) async throws -> URL? {
3636
let cacheKey = CacheKey(compiler: compiler, workingDirectory: workingDirectory)
3737
if let cached = cache[cacheKey] {
3838
return try cached.get()
3939
}
40+
4041
let computed: Result<URL?, Error>
4142
do {
42-
computed = .success(
43-
try await resolveSwiftlyTrampolineImpl(compiler: compiler, workingDirectory: workingDirectory)
44-
)
43+
var resolved = try await resolveSwiftlyTrampoline(compiler: compiler, workingDirectory: workingDirectory)
44+
if resolved == nil {
45+
resolved = try await resolveXcrunTrampoline(compiler: compiler, workingDirectory: workingDirectory)
46+
}
47+
48+
computed = .success(resolved)
4549
} catch {
4650
computed = .failure(error)
4751
}
52+
4853
cache[cacheKey] = computed
4954
return try computed.get()
5055
}
5156

52-
private func resolveSwiftlyTrampolineImpl(compiler: URL, workingDirectory: URL?) async throws -> URL? {
57+
private func resolveSwiftlyTrampoline(compiler: URL, workingDirectory: URL?) async throws -> URL? {
5358
let realpath = try compiler.realpath
5459
guard realpath.lastPathComponent == "swiftly" else {
5560
return nil
5661
}
62+
5763
let swiftlyResult = try await Process.run(
5864
arguments: [realpath.filePath, "use", "-p"],
5965
workingDirectory: try AbsolutePath(validatingOrNil: workingDirectory?.filePath)
6066
)
6167
let swiftlyToolchain = URL(
6268
fileURLWithPath: try swiftlyResult.utf8Output().trimmingCharacters(in: .whitespacesAndNewlines)
6369
)
70+
6471
let resolvedCompiler = swiftlyToolchain.appending(components: "usr", "bin", compiler.lastPathComponent)
6572
if FileManager.default.fileExists(at: resolvedCompiler) {
6673
return resolvedCompiler
6774
}
6875
return nil
6976
}
7077

78+
private func resolveXcrunTrampoline(compiler: URL, workingDirectory: URL?) async throws -> URL? {
79+
guard Platform.current == .darwin, compiler.deletingLastPathComponent() == URL(filePath: "/usr/bin/") else {
80+
return nil
81+
}
82+
83+
let xcrunResult = try await Process.run(
84+
arguments: ["xcrun", "-f", compiler.lastPathComponent],
85+
workingDirectory: try AbsolutePath(validatingOrNil: workingDirectory?.filePath)
86+
)
87+
88+
let resolvedCompiler = URL(
89+
fileURLWithPath: try xcrunResult.utf8Output().trimmingCharacters(in: .whitespacesAndNewlines)
90+
)
91+
if FileManager.default.fileExists(at: resolvedCompiler) {
92+
return resolvedCompiler
93+
}
94+
return nil
95+
}
96+
7197
func clearCache() {
7298
cache.removeAll()
7399
}

Tests/SourceKitLSPTests/CompilationDatabaseTests.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@ final class CompilationDatabaseTests: SourceKitLSPTestCase {
261261
libIndexStore: nil
262262
)
263263
let toolchainRegistry = ToolchainRegistry(toolchains: [
264-
try await unwrap(ToolchainRegistry.forTesting.default), fakeToolchain,
264+
defaultToolchain, fakeToolchain,
265265
])
266266

267-
// We need to create a file for the swift executable because `SwiftlyResolver` checks for its presence.
267+
// We need to create a file for the swift executable because `SwiftToolchainResolver` checks for its presence.
268268
try FileManager.default.createDirectory(
269269
at: XCTUnwrap(fakeToolchain.swift).deletingLastPathComponent(),
270270
withIntermediateDirectories: true
@@ -389,6 +389,43 @@ final class CompilationDatabaseTests: SourceKitLSPTestCase {
389389
)
390390
XCTAssertEqual(definition?.locations, [try project.location(from: "1️⃣", to: "2️⃣", in: "header.h")])
391391
}
392+
393+
func testLookThroughXcrun() async throws {
394+
try SkipUnless.platformIsDarwin("xcrun is macOS only")
395+
396+
try await withTestScratchDir { scratchDirectory in
397+
let toolchainRegistry = try XCTUnwrap(ToolchainRegistry.forTesting)
398+
399+
let project = try await MultiFileTestProject(
400+
files: [
401+
"test.swift": """
402+
#warning("Test warning")
403+
""",
404+
"compile_commands.json": """
405+
[
406+
{
407+
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
408+
"arguments": [
409+
"/usr/bin/swiftc",
410+
"$TEST_DIR_BACKSLASH_ESCAPED/test.swift",
411+
\(defaultSDKArgs)
412+
],
413+
"file": "test.swift",
414+
"output": "$TEST_DIR_BACKSLASH_ESCAPED/test.swift.o"
415+
}
416+
]
417+
""",
418+
],
419+
toolchainRegistry: toolchainRegistry
420+
)
421+
422+
let (uri, _) = try project.openDocument("test.swift")
423+
let diagnostics = try await project.testClient.send(
424+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
425+
)
426+
XCTAssertEqual(diagnostics.fullReport?.items.map(\.message), ["Test warning"])
427+
}
428+
}
392429
}
393430

394431
private let defaultSDKArgs: String = {

0 commit comments

Comments
 (0)