From 8ac9cd640d7153103aca202a72106f7e4fff03ab Mon Sep 17 00:00:00 2001 From: Ben Barham Date: Thu, 20 Nov 2025 15:33:32 +1000 Subject: [PATCH] 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. --- Package.swift | 8 ++-- Sources/BuildServerIntegration/CMakeLists.txt | 4 +- .../JSONCompilationDatabaseBuildServer.swift | 24 +++++----- ...ver.swift => SwiftToolchainResolver.swift} | 48 ++++++++++++++----- .../SwiftSourceKitClientPlugin/CMakeLists.txt | 4 +- .../ClientPlugin.swift | 4 +- Sources/SwiftSourceKitPlugin/CMakeLists.txt | 2 +- Sources/SwiftSourceKitPlugin/Plugin.swift | 6 ++- .../SwiftSourceKitPluginCommon/CMakeLists.txt | 3 -- ...namicallyLoadedSourceKitdD+forPlugin.swift | 1 - .../CompilationDatabaseTests.swift | 41 +++++++++++++++- 11 files changed, 103 insertions(+), 42 deletions(-) rename Sources/BuildServerIntegration/{SwiftlyResolver.swift => SwiftToolchainResolver.swift} (54%) diff --git a/Package.swift b/Package.swift index fabf0c1a..657271d2 100644 --- a/Package.swift +++ b/Package.swift @@ -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", ]) diff --git a/Sources/BuildServerIntegration/CMakeLists.txt b/Sources/BuildServerIntegration/CMakeLists.txt index b3ab347d..68fe95c5 100644 --- a/Sources/BuildServerIntegration/CMakeLists.txt +++ b/Sources/BuildServerIntegration/CMakeLists.txt @@ -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 diff --git a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift index d43fe6f0..8000b73e 100644 --- a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift +++ b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift @@ -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 { diff --git a/Sources/BuildServerIntegration/SwiftlyResolver.swift b/Sources/BuildServerIntegration/SwiftToolchainResolver.swift similarity index 54% rename from Sources/BuildServerIntegration/SwiftlyResolver.swift rename to Sources/BuildServerIntegration/SwiftToolchainResolver.swift index 48767741..f53c9fb2 100644 --- a/Sources/BuildServerIntegration/SwiftlyResolver.swift +++ b/Sources/BuildServerIntegration/SwiftToolchainResolver.swift @@ -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> = 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 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() } diff --git a/Sources/SwiftSourceKitClientPlugin/CMakeLists.txt b/Sources/SwiftSourceKitClientPlugin/CMakeLists.txt index 00293749..bfc69633 100644 --- a/Sources/SwiftSourceKitClientPlugin/CMakeLists.txt +++ b/Sources/SwiftSourceKitClientPlugin/CMakeLists.txt @@ -5,15 +5,15 @@ set_target_properties(SwiftSourceKitClientPlugin PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) target_compile_options(SwiftSourceKitClientPlugin PRIVATE $<$: + "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 $<$>:FoundationXML>) diff --git a/Sources/SwiftSourceKitClientPlugin/ClientPlugin.swift b/Sources/SwiftSourceKitClientPlugin/ClientPlugin.swift index 2a858849..3b1c62fc 100644 --- a/Sources/SwiftSourceKitClientPlugin/ClientPlugin.swift +++ b/Sources/SwiftSourceKitClientPlugin/ClientPlugin.swift @@ -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") diff --git a/Sources/SwiftSourceKitPlugin/CMakeLists.txt b/Sources/SwiftSourceKitPlugin/CMakeLists.txt index 4d45a136..29112588 100644 --- a/Sources/SwiftSourceKitPlugin/CMakeLists.txt +++ b/Sources/SwiftSourceKitPlugin/CMakeLists.txt @@ -33,9 +33,9 @@ set_target_properties(SwiftSourceKitPlugin PROPERTIES target_compile_options(SwiftSourceKitPlugin PRIVATE $<$: "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" >) diff --git a/Sources/SwiftSourceKitPlugin/Plugin.swift b/Sources/SwiftSourceKitPlugin/Plugin.swift index c03a1d95..7d22fda8 100644 --- a/Sources/SwiftSourceKitPlugin/Plugin.swift +++ b/Sources/SwiftSourceKitPlugin/Plugin.swift @@ -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 ) { - 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" { diff --git a/Sources/SwiftSourceKitPluginCommon/CMakeLists.txt b/Sources/SwiftSourceKitPluginCommon/CMakeLists.txt index 200350a5..8ead028d 100644 --- a/Sources/SwiftSourceKitPluginCommon/CMakeLists.txt +++ b/Sources/SwiftSourceKitPluginCommon/CMakeLists.txt @@ -5,16 +5,13 @@ add_library(SwiftSourceKitPluginCommon STATIC target_compile_options(SwiftSourceKitPluginCommon PRIVATE $<$: "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 $<$>:FoundationXML>) diff --git a/Sources/SwiftSourceKitPluginCommon/DynamicallyLoadedSourceKitdD+forPlugin.swift b/Sources/SwiftSourceKitPluginCommon/DynamicallyLoadedSourceKitdD+forPlugin.swift index c55884c2..5e844546 100644 --- a/Sources/SwiftSourceKitPluginCommon/DynamicallyLoadedSourceKitdD+forPlugin.swift +++ b/Sources/SwiftSourceKitPluginCommon/DynamicallyLoadedSourceKitdD+forPlugin.swift @@ -11,7 +11,6 @@ //===----------------------------------------------------------------------===// import Foundation -@_spi(SourceKitLSP) import SKLogging package import SourceKitD import SwiftExtensions diff --git a/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift b/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift index d061c66c..eb396615 100644 --- a/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift +++ b/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift @@ -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 = {