Skip to content
Open
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
4 changes: 2 additions & 2 deletions Sources/BuildServerIntegration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ add_library(BuildServerIntegration STATIC
LegacyBuildServer.swift
MainFilesProvider.swift
SplitShellCommand.swift
SwiftlyResolver.swift
SwiftPMBuildServer.swift)
SwiftPMBuildServer.swift
SwiftToolchainResolver.swift)
set_target_properties(BuildServerIntegration PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
target_link_libraries(BuildServerIntegration PUBLIC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,20 @@ fileprivate extension CompilationDatabaseCompileCommand {
///
/// The absence of a compiler means we have an empty command line, which should never happen.
///
/// If the compiler is a symlink to `swiftly`, it uses `swiftlyResolver` to find the corresponding executable in a
/// real toolchain and returns that executable.
func compiler(swiftlyResolver: SwiftlyResolver, compileCommandsDirectory: URL) async -> String? {
/// If the compiler is a symlink to `swiftly` or in `/usr/bin` on macOS, it uses `toolchainResolver` to find the
/// corresponding executable in a real toolchain and returns that executable.
func compiler(toolchainResolver: SwiftToolchainResolver, compileCommandsDirectory: URL) async -> String? {
guard let compiler = commandLine.first else {
return nil
}
let swiftlyResolved = await orLog("Resolving swiftly") {
try await swiftlyResolver.resolve(
let resolved = await orLog("Resolving compiler") {
try await toolchainResolver.resolve(
compiler: URL(fileURLWithPath: compiler),
workingDirectory: directoryURL(compileCommandsDirectory: compileCommandsDirectory)
)?.filePath
}
if let swiftlyResolved {
return swiftlyResolved
if let resolved {
return resolved
}
return compiler
}
Expand Down Expand Up @@ -74,7 +74,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
/// finds the compilation database in a build directory.
private var configDirectory: URL

private let swiftlyResolver = SwiftlyResolver()
private let toolchainResolver = SwiftToolchainResolver()

// Watch for all all changes to `compile_commands.json` and `compile_flags.txt` instead of just the one at
// `configPath` so that we cover the following semi-common scenario:
Expand Down Expand Up @@ -124,7 +124,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse {
let compilers = Set(
await compdb.commands.asyncCompactMap { (command) -> String? in
await command.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
await command.compiler(toolchainResolver: toolchainResolver, compileCommandsDirectory: configDirectory)
}
).sorted { $0 < $1 }
let targets = try await compilers.asyncMap { compiler in
Expand Down Expand Up @@ -155,7 +155,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
}
let commandsWithRequestedCompilers = await compdb.commands.lazy.asyncFilter { command in
return await targetCompiler
== command.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
== command.compiler(toolchainResolver: toolchainResolver, compileCommandsDirectory: configDirectory)
}
let sources = commandsWithRequestedCompilers.map {
SourceItem(uri: $0.uri(compileCommandsDirectory: configDirectory), kind: .file, generated: false)
Expand All @@ -171,7 +171,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
self.reloadCompilationDatabase()
}
if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == ".swift-version" }) {
await swiftlyResolver.clearCache()
await toolchainResolver.clearCache()
connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil))
}
}
Expand All @@ -185,7 +185,7 @@ package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer {
) async throws -> TextDocumentSourceKitOptionsResponse? {
let targetCompiler = try request.target.compileCommandsCompiler
let command = await compdb[request.textDocument.uri].asyncFilter {
return await $0.compiler(swiftlyResolver: swiftlyResolver, compileCommandsDirectory: configDirectory)
return await $0.compiler(toolchainResolver: toolchainResolver, compileCommandsDirectory: configDirectory)
== targetCompiler
}.first
guard let command else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,56 +18,82 @@ import TSCExtensions
import struct TSCBasic.AbsolutePath
import class TSCBasic.Process

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

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

/// Check if `compiler` is a symlink to `swiftly`. If so, find the executable in the toolchain that swiftly resolves
/// to within the given working directory and return the URL of the corresponding compiler in that toolchain.
/// If `compiler` does not resolve to `swiftly`, return `nil`.
/// Check if `compiler` is a symlink to `swiftly` or in `/usr/bin` on macOS. If so, find the executable in the
/// toolchain that would be resolved to within the given working directory and return the URL of the corresponding
/// compiler in that toolchain. If `compiler` does not resolve to `swiftly` or `/usr/bin` on macOS, return `nil`.
func resolve(compiler: URL, workingDirectory: URL?) async throws -> URL? {
let cacheKey = CacheKey(compiler: compiler, workingDirectory: workingDirectory)
if let cached = cache[cacheKey] {
return try cached.get()
}

let computed: Result<URL?, Error>
do {
computed = .success(
try await resolveSwiftlyTrampolineImpl(compiler: compiler, workingDirectory: workingDirectory)
)
var resolved = try await resolveSwiftlyTrampoline(compiler: compiler, workingDirectory: workingDirectory)
if resolved == nil {
resolved = try await resolveXcrunTrampoline(compiler: compiler, workingDirectory: workingDirectory)
}

computed = .success(resolved)
} catch {
computed = .failure(error)
}

cache[cacheKey] = computed
return try computed.get()
}

private func resolveSwiftlyTrampolineImpl(compiler: URL, workingDirectory: URL?) async throws -> URL? {
private func resolveSwiftlyTrampoline(compiler: URL, workingDirectory: URL?) async throws -> URL? {
let realpath = try compiler.realpath
guard realpath.lastPathComponent == "swiftly" else {
return nil
}

let swiftlyResult = try await Process.run(
arguments: [realpath.filePath, "use", "-p"],
workingDirectory: try AbsolutePath(validatingOrNil: workingDirectory?.filePath)
)
let swiftlyToolchain = URL(
fileURLWithPath: try swiftlyResult.utf8Output().trimmingCharacters(in: .whitespacesAndNewlines)
)

let resolvedCompiler = swiftlyToolchain.appending(components: "usr", "bin", compiler.lastPathComponent)
if FileManager.default.fileExists(at: resolvedCompiler) {
return resolvedCompiler
}
return nil
}

private func resolveXcrunTrampoline(compiler: URL, workingDirectory: URL?) async throws -> URL? {
guard Platform.current == .darwin, compiler.deletingLastPathComponent() == URL(filePath: "/usr/bin/") else {
return nil
}

let xcrunResult = try await Process.run(
arguments: ["xcrun", "-f", compiler.lastPathComponent],
workingDirectory: try AbsolutePath(validatingOrNil: workingDirectory?.filePath)
)

let resolvedCompiler = URL(
fileURLWithPath: try xcrunResult.utf8Output().trimmingCharacters(in: .whitespacesAndNewlines)
)
if FileManager.default.fileExists(at: resolvedCompiler) {
return resolvedCompiler
}
return nil
}

func clearCache() {
cache.removeAll()
}
Expand Down
41 changes: 39 additions & 2 deletions Tests/SourceKitLSPTests/CompilationDatabaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,10 @@ final class CompilationDatabaseTests: SourceKitLSPTestCase {
libIndexStore: nil
)
let toolchainRegistry = ToolchainRegistry(toolchains: [
try await unwrap(ToolchainRegistry.forTesting.default), fakeToolchain,
defaultToolchain, fakeToolchain,
])

// We need to create a file for the swift executable because `SwiftlyResolver` checks for its presence.
// We need to create a file for the swift executable because `SwiftToolchainResolver` checks for its presence.
try FileManager.default.createDirectory(
at: XCTUnwrap(fakeToolchain.swift).deletingLastPathComponent(),
withIntermediateDirectories: true
Expand Down Expand Up @@ -389,6 +389,43 @@ final class CompilationDatabaseTests: SourceKitLSPTestCase {
)
XCTAssertEqual(definition?.locations, [try project.location(from: "1️⃣", to: "2️⃣", in: "header.h")])
}

func testLookThroughXcrun() async throws {
try SkipUnless.platformIsDarwin("xcrun is macOS only")

try await withTestScratchDir { scratchDirectory in
let toolchainRegistry = try XCTUnwrap(ToolchainRegistry.forTesting)

let project = try await MultiFileTestProject(
files: [
"test.swift": """
#warning("Test warning")
""",
"compile_commands.json": """
[
{
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
"arguments": [
"/usr/bin/swiftc",
"$TEST_DIR_BACKSLASH_ESCAPED/test.swift",
\(defaultSDKArgs)
],
"file": "test.swift",
"output": "$TEST_DIR_BACKSLASH_ESCAPED/test.swift.o"
}
]
""",
],
toolchainRegistry: toolchainRegistry
)

let (uri, _) = try project.openDocument("test.swift")
let diagnostics = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(diagnostics.fullReport?.items.map(\.message), ["Test warning"])
}
}
}

private let defaultSDKArgs: String = {
Expand Down