Merge pull request #2362 from bnbarham/xcrun-usr-bin-swift

Resolve `/usr/bin/*` shims on macOS
This commit is contained in:
Ben Barham
2025-12-03 20:06:58 +10:00
committed by GitHub
11 changed files with 103 additions and 42 deletions

View File

@@ -526,13 +526,14 @@ var targets: [Target] = [
"SourceKitDForPlugin",
"SwiftExtensionsForPlugin",
"SwiftSourceKitPluginCommon",
.product(name: "_SKLoggingForPlugin", package: "swift-tools-protocols"),
],
exclude: ["CMakeLists.txt"],
swiftSettings: [
.unsafeFlags([
"-module-alias", "SKLogging=_SKLoggingForPlugin",
"-module-alias", "SourceKitD=SourceKitDForPlugin",
"-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin",
"-module-alias", "ToolsProtocolsSwiftExtensions=_ToolsProtocolsSwiftExtensionsForPlugin",
])
],
linkerSettings: sourcekitLSPLinkSettings
@@ -546,15 +547,12 @@ var targets: [Target] = [
"Csourcekitd",
"SourceKitDForPlugin",
"SwiftExtensionsForPlugin",
.product(name: "_SKLoggingForPlugin", package: "swift-tools-protocols"),
],
exclude: ["CMakeLists.txt"],
swiftSettings: [
.unsafeFlags([
"-module-alias", "SourceKitD=SourceKitDForPlugin",
"-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin",
"-module-alias", "ToolsProtocolsSwiftExtensions=_ToolsProtocolsSwiftExtensionsForPlugin",
"-module-alias", "SKLogging=_SKLoggingForPlugin",
])
]
),
@@ -577,9 +575,9 @@ var targets: [Target] = [
swiftSettings: [
.unsafeFlags([
"-module-alias", "CompletionScoring=CompletionScoringForPlugin",
"-module-alias", "SKLogging=_SKLoggingForPlugin",
"-module-alias", "SKUtilities=SKUtilitiesForPlugin",
"-module-alias", "SourceKitD=SourceKitDForPlugin",
"-module-alias", "SKLogging=_SKLoggingForPlugin",
"-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin",
"-module-alias", "ToolsProtocolsSwiftExtensions=_ToolsProtocolsSwiftExtensionsForPlugin",
])

View File

@@ -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

View File

@@ -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
}
@@ -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:
@@ -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
@@ -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)
@@ -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))
}
}
@@ -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 {

View File

@@ -18,10 +18,10 @@ 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?
@@ -29,31 +29,37 @@ actor SwiftlyResolver {
private var cache: LRUCache<CacheKey, Result<URL?, any 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?, any 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)
@@ -61,6 +67,7 @@ actor SwiftlyResolver {
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
@@ -68,6 +75,25 @@ actor SwiftlyResolver {
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()
}

View File

@@ -5,15 +5,15 @@ set_target_properties(SwiftSourceKitClientPlugin PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
target_compile_options(SwiftSourceKitClientPlugin PRIVATE
$<$<COMPILE_LANGUAGE:Swift>:
"SHELL:-module-alias SKLogging=_SKLoggingForPlugin"
"SHELL:-module-alias SourceKitD=SourceKitDForPlugin"
"SHELL:-module-alias SwiftExtensions=SwiftExtensionsForPlugin"
"SHELL:-module-alias ToolsProtocolsSwiftExtensions=_ToolsProtocolsSwiftExtensionsForPlugin"
>)
target_link_libraries(SwiftSourceKitClientPlugin PRIVATE
Csourcekitd
SourceKitD
SwiftExtensions
SwiftToolsProtocols::ToolsProtocolsSwiftExtensions
SwiftToolsProtocols::_SKLoggingForPlugin
SwiftSourceKitPluginCommon
$<$<NOT:$<PLATFORM_ID:Darwin>>:FoundationXML>)

View File

@@ -12,6 +12,7 @@
public import Csourcekitd
import Foundation
@_spi(SourceKitLSP) import SKLogging
import SourceKitD
import SwiftExtensions
import SwiftSourceKitPluginCommon
@@ -20,7 +21,8 @@ import SwiftSourceKitPluginCommon
/// loaded from.
@_cdecl("sourcekitd_plugin_initialize")
public func sourcekitd_plugin_initialize(_ params: sourcekitd_api_plugin_initialize_params_t) {
fatalError("sourcekitd_plugin_initialize has been removed in favor of sourcekitd_plugin_initialize_2")
LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.client-plugin")
logger.fault("sourcekitd_plugin_initialize has been removed in favor of sourcekitd_plugin_initialize_2")
}
@_cdecl("sourcekitd_plugin_initialize_2")

View File

@@ -33,9 +33,9 @@ set_target_properties(SwiftSourceKitPlugin PROPERTIES
target_compile_options(SwiftSourceKitPlugin PRIVATE
$<$<COMPILE_LANGUAGE:Swift>:
"SHELL:-module-alias CompletionScoring=CompletionScoringForPlugin"
"SHELL:-module-alias SKLogging=_SKLoggingForPlugin"
"SHELL:-module-alias SKUtilities=SKUtilitiesForPlugin"
"SHELL:-module-alias SourceKitD=SourceKitDForPlugin"
"SHELL:-module-alias SKLogging=_SKLoggingForPlugin"
"SHELL:-module-alias SwiftExtensions=SwiftExtensionsForPlugin"
"SHELL:-module-alias ToolsProtocolsSwiftExtensions=_ToolsProtocolsSwiftExtensionsForPlugin"
>)

View File

@@ -162,7 +162,8 @@ final class RequestHandler: Sendable {
/// loaded from.
@_cdecl("sourcekitd_plugin_initialize")
public func sourcekitd_plugin_initialize(_ params: sourcekitd_api_plugin_initialize_params_t) {
fatalError("sourcekitd_plugin_initialize has been removed in favor of sourcekitd_plugin_initialize_2")
LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.service-plugin")
logger.fault("sourcekitd_plugin_initialize has been removed in favor of sourcekitd_plugin_initialize_2")
}
#if canImport(Darwin)
@@ -210,7 +211,8 @@ public func sourcekitd_plugin_initialize_2(
_ params: sourcekitd_api_plugin_initialize_params_t,
_ parentLibraryPath: UnsafePointer<CChar>
) {
LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.plugin")
LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.service-plugin")
let parentLibraryPath = String(cString: parentLibraryPath)
#if canImport(Darwin)
if parentLibraryPath == "SOURCEKIT_LSP_PLUGIN_PARENT_LIBRARY_RTLD_DEFAULT" {

View File

@@ -5,16 +5,13 @@ add_library(SwiftSourceKitPluginCommon STATIC
target_compile_options(SwiftSourceKitPluginCommon PRIVATE
$<$<COMPILE_LANGUAGE:Swift>:
"SHELL:-module-alias SourceKitD=SourceKitDForPlugin"
"SHELL:-module-alias SKLogging=_SKLoggingForPlugin"
"SHELL:-module-alias SwiftExtensions=SwiftExtensionsForPlugin"
"SHELL:-module-alias ToolsProtocolsSwiftExtensions=_ToolsProtocolsSwiftExtensionsForPlugin"
>)
set_target_properties(SwiftSourceKitPluginCommon PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
target_link_libraries(SwiftSourceKitPluginCommon PRIVATE
Csourcekitd
SourceKitDForPlugin
SwiftToolsProtocols::_SKLoggingForPlugin
SwiftExtensionsForPlugin
SwiftToolsProtocols::_ToolsProtocolsSwiftExtensionsForPlugin
$<$<NOT:$<PLATFORM_ID:Darwin>>:FoundationXML>)

View File

@@ -11,7 +11,6 @@
//===----------------------------------------------------------------------===//
import Foundation
@_spi(SourceKitLSP) import SKLogging
package import SourceKitD
import SwiftExtensions

View File

@@ -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
@@ -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 = {