diff --git a/Sources/SKCore/CompilationDatabase.swift b/Sources/SKCore/CompilationDatabase.swift index a95d76c1..20e944b2 100644 --- a/Sources/SKCore/CompilationDatabase.swift +++ b/Sources/SKCore/CompilationDatabase.swift @@ -15,6 +15,7 @@ import SKSupport import struct TSCBasic.AbsolutePath import protocol TSCBasic.FileSystem +import struct TSCBasic.RelativePath import var TSCBasic.localFileSystem import func TSCBasic.resolveSymlinks @@ -67,14 +68,28 @@ public protocol CompilationDatabase { var allCommands: AnySequence { get } } -/// Loads the compilation database located in `directory`, if any. +/// Loads the compilation database located in `directory`, if one can be found in `additionalSearchPaths` or in the default search paths of "." and "build". public func tryLoadCompilationDatabase( directory: AbsolutePath, + additionalSearchPaths: [RelativePath] = [], _ fileSystem: FileSystem = localFileSystem ) -> CompilationDatabase? { + let searchPaths = + additionalSearchPaths + [ + // These default search paths match the behavior of `clangd` + try! RelativePath(validating: "."), + try! RelativePath(validating: "build"), + ] return - (try? JSONCompilationDatabase(directory: directory, fileSystem)) - ?? (try? FixedCompilationDatabase(directory: directory, fileSystem)) + try! searchPaths + .lazy + .map { directory.appending($0) } + .compactMap { + try + (JSONCompilationDatabase(directory: $0, fileSystem) + ?? FixedCompilationDatabase(directory: $0, fileSystem)) + } + .first } /// Fixed clang-compatible compilation database (compile_flags.txt). @@ -99,13 +114,21 @@ public struct FixedCompilationDatabase: CompilationDatabase, Equatable { } extension FixedCompilationDatabase { - public init(directory: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { + /// Loads the compilation database located in `directory`, if any. + /// - Returns: `nil` if `compile_flags.txt` was not found + public init?(directory: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { let path = directory.appending(component: "compile_flags.txt") try self.init(file: path, fileSystem) } - public init(file: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { + /// Loads the compilation database from `file` + /// - Returns: `nil` if the file does not exist + public init?(file: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { self.directory = file.dirname + + guard fileSystem.exists(file) else { + return nil + } let bytes = try fileSystem.readFileContents(file) var fixedArgs: [String] = ["clang"] @@ -185,12 +208,20 @@ extension JSONCompilationDatabase: Codable { } extension JSONCompilationDatabase { - public init(directory: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { + /// Loads the compilation database located in `directory`, if any. + /// + /// - Returns: `nil` if `compile_commands.json` was not found + public init?(directory: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { let path = directory.appending(component: "compile_commands.json") try self.init(file: path, fileSystem) } - public init(file: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { + /// Loads the compilation database from `file` + /// - Returns: `nil` if the file does not exist + public init?(file: AbsolutePath, _ fileSystem: FileSystem = localFileSystem) throws { + guard fileSystem.exists(file) else { + return nil + } let bytes = try fileSystem.readFileContents(file) try bytes.withUnsafeData { data in self = try JSONDecoder().decode(JSONCompilationDatabase.self, from: data) diff --git a/Sources/SKCore/CompilationDatabaseBuildSystem.swift b/Sources/SKCore/CompilationDatabaseBuildSystem.swift index ddbe1791..d0a3c6f7 100644 --- a/Sources/SKCore/CompilationDatabaseBuildSystem.swift +++ b/Sources/SKCore/CompilationDatabaseBuildSystem.swift @@ -19,6 +19,7 @@ import SKSupport import struct Foundation.URL import struct TSCBasic.AbsolutePath import protocol TSCBasic.FileSystem +import struct TSCBasic.RelativePath import var TSCBasic.localFileSystem /// A `BuildSystem` based on loading clang-compatible compilation database(s). @@ -44,6 +45,8 @@ public actor CompilationDatabaseBuildSystem { let projectRoot: AbsolutePath? + let searchPaths: [RelativePath] + let fileSystem: FileSystem /// The URIs for which the delegate has registered for change notifications, @@ -70,11 +73,12 @@ public actor CompilationDatabaseBuildSystem { return nil } - public init(projectRoot: AbsolutePath? = nil, fileSystem: FileSystem = localFileSystem) { + public init(projectRoot: AbsolutePath? = nil, searchPaths: [RelativePath], fileSystem: FileSystem = localFileSystem) { self.fileSystem = fileSystem self.projectRoot = projectRoot + self.searchPaths = searchPaths if let path = projectRoot { - self.compdb = tryLoadCompilationDatabase(directory: path, fileSystem) + self.compdb = tryLoadCompilationDatabase(directory: path, additionalSearchPaths: searchPaths, fileSystem) } } } @@ -122,7 +126,7 @@ extension CompilationDatabaseBuildSystem: BuildSystem { var dir = path while !dir.isRoot { dir = dir.parentDirectory - if let db = tryLoadCompilationDatabase(directory: dir, fileSystem) { + if let db = tryLoadCompilationDatabase(directory: dir, additionalSearchPaths: searchPaths, fileSystem) { compdb = db break } @@ -150,7 +154,11 @@ extension CompilationDatabaseBuildSystem: BuildSystem { private func reloadCompilationDatabase() async { guard let projectRoot = self.projectRoot else { return } - self.compdb = tryLoadCompilationDatabase(directory: projectRoot, self.fileSystem) + self.compdb = tryLoadCompilationDatabase( + directory: projectRoot, + additionalSearchPaths: searchPaths, + self.fileSystem + ) if let delegate = self.delegate { var changedFiles = Set() diff --git a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift index 3de53017..6557c209 100644 --- a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift +++ b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift @@ -55,7 +55,7 @@ public final class SKSwiftPMTestWorkspace { /// Connection to the language server. public let testClient: TestSourceKitLSPClient - /// When `testServer` is not `nil`, the workspace will be opened in that server, otherwise a new server will be created for the workspace + /// When `testClient` is not `nil`, the workspace will be opened in that client's server, otherwise a new client will be created for the workspace public init( projectDir: URL, tmpDir: URL, diff --git a/Sources/SKTestSupport/SKTibsTestWorkspace.swift b/Sources/SKTestSupport/SKTibsTestWorkspace.swift index 5614c365..68d6b475 100644 --- a/Sources/SKTestSupport/SKTibsTestWorkspace.swift +++ b/Sources/SKTestSupport/SKTibsTestWorkspace.swift @@ -24,6 +24,7 @@ import XCTest import enum PackageLoading.Platform import struct PackageModel.BuildFlags import struct TSCBasic.AbsolutePath +import struct TSCBasic.RelativePath public typealias URL = Foundation.URL @@ -83,7 +84,10 @@ public final class SKTibsTestWorkspace { func initWorkspace(clientCapabilities: ClientCapabilities) async throws { let buildPath = try AbsolutePath(validating: builder.buildRoot.path) - let buildSystem = CompilationDatabaseBuildSystem(projectRoot: buildPath) + let buildSystem = CompilationDatabaseBuildSystem( + projectRoot: buildPath, + searchPaths: try [RelativePath(validating: ".")] + ) let indexDelegate = SourceKitIndexDelegate() tibsWorkspace.delegate = indexDelegate diff --git a/Sources/SourceKitLSP/SourceKitServer+Options.swift b/Sources/SourceKitLSP/SourceKitServer+Options.swift index c59e0496..6c2b95f7 100644 --- a/Sources/SourceKitLSP/SourceKitServer+Options.swift +++ b/Sources/SourceKitLSP/SourceKitServer+Options.swift @@ -16,6 +16,7 @@ import SKCore import SKSupport import struct TSCBasic.AbsolutePath +import struct TSCBasic.RelativePath extension SourceKitServer { @@ -29,6 +30,9 @@ extension SourceKitServer { /// Additional arguments to pass to `clangd` on the command-line. public var clangdOptions: [String] + /// Additional paths to search for a compilation database, relative to a workspace root. + public var compilationDatabaseSearchPaths: [RelativePath] + /// Additional options for the index. public var indexOptions: IndexOptions @@ -48,6 +52,7 @@ extension SourceKitServer { public init( buildSetup: BuildSetup = .default, clangdOptions: [String] = [], + compilationDatabaseSearchPaths: [RelativePath] = [], indexOptions: IndexOptions = .init(), completionOptions: SKCompletionOptions = .init(), generatedInterfacesPath: AbsolutePath = defaultDirectoryForGeneratedInterfaces, @@ -55,6 +60,7 @@ extension SourceKitServer { ) { self.buildSetup = buildSetup self.clangdOptions = clangdOptions + self.compilationDatabaseSearchPaths = compilationDatabaseSearchPaths self.indexOptions = indexOptions self.completionOptions = completionOptions self.generatedInterfacesPath = generatedInterfacesPath diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index 816080fb..65fe9983 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -597,7 +597,7 @@ private var notificationIDForLogging: Int = 0 /// On every call, returns a new unique number that can be used to identify a notification. /// -/// This is needed so we can consistently refer to a notification using the `category` of the logger. +/// This is needed so we can consistently refer to a notification using the `category` of the logger. /// Requests don't need this since they already have a unique ID in the LSP protocol. private func getNextNotificationIDForLogging() -> Int { return notificationIDForLoggingLock.withLock { @@ -648,7 +648,7 @@ extension SourceKitServer: MessageHandler { await self.withLanguageServiceAndWorkspace(for: notification, notificationHandler: self.willSaveDocument) case let notification as DidSaveTextDocumentNotification: await self.withLanguageServiceAndWorkspace(for: notification, notificationHandler: self.didSaveDocument) - // IMPORTANT: When adding a new entry to this switch, also add it to the `TaskMetadata` initializer. + // IMPORTANT: When adding a new entry to this switch, also add it to the `TaskMetadata` initializer. default: break } @@ -771,7 +771,7 @@ extension SourceKitServer: MessageHandler { requestHandler: self.documentDiagnostic, fallback: .full(.init(items: [])) ) - // IMPORTANT: When adding a new entry to this switch, also add it to the `TaskMetadata` initializer. + // IMPORTANT: When adding a new entry to this switch, also add it to the `TaskMetadata` initializer. default: reply(.failure(ResponseError.methodNotFound(R.method))) } @@ -860,6 +860,7 @@ extension SourceKitServer { capabilityRegistry: capabilityRegistry, toolchainRegistry: self.toolchainRegistry, buildSetup: self.options.buildSetup, + compilationDatabaseSearchPaths: self.options.compilationDatabaseSearchPaths, indexOptions: self.options.indexOptions, reloadPackageStatusCallback: { status in guard capabilityRegistry.clientCapabilities.window?.workDoneProgress ?? false else { diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index c358b904..9adcea0f 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -18,6 +18,7 @@ import SKSupport import SKSwiftPMWorkspace import struct TSCBasic.AbsolutePath +import struct TSCBasic.RelativePath /// Same as `??` but allows the right-hand side of the operator to 'await'. fileprivate func firstNonNil(_ optional: T?, _ defaultValue: @autoclosure () async throws -> T) async rethrows -> T { @@ -102,6 +103,7 @@ public final class Workspace { capabilityRegistry: CapabilityRegistry, toolchainRegistry: ToolchainRegistry, buildSetup: BuildSetup, + compilationDatabaseSearchPaths: [RelativePath], indexOptions: IndexOptions = IndexOptions(), reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void ) async throws { @@ -117,7 +119,7 @@ public final class Workspace { ) { buildSystem = swiftpm } else { - buildSystem = CompilationDatabaseBuildSystem(projectRoot: rootPath) + buildSystem = CompilationDatabaseBuildSystem(projectRoot: rootPath, searchPaths: compilationDatabaseSearchPaths) } } else { // We assume that workspaces are directories. This is only true for URLs not for URIs in general. diff --git a/Sources/sourcekit-lsp/SourceKitLSP.swift b/Sources/sourcekit-lsp/SourceKitLSP.swift index 9ce8719f..e6fe22bb 100644 --- a/Sources/sourcekit-lsp/SourceKitLSP.swift +++ b/Sources/sourcekit-lsp/SourceKitLSP.swift @@ -22,6 +22,7 @@ import SKSupport import SourceKitLSP import struct TSCBasic.AbsolutePath +import struct TSCBasic.RelativePath import var TSCBasic.localFileSystem extension AbsolutePath: ExpressibleByArgument { @@ -48,6 +49,18 @@ extension AbsolutePath: ExpressibleByArgument { } } +extension RelativePath: ExpressibleByArgument { + public init?(argument: String) { + let path = try? RelativePath(validating: argument) + + guard let path = path else { + return nil + } + + self = path + } +} + extension PathPrefixMapping: ExpressibleByArgument { public init?(argument: String) { guard let eqIndex = argument.firstIndex(of: "=") else { return nil } @@ -137,6 +150,14 @@ struct SourceKitLSP: ParsableCommand { ) var indexPrefixMappings = [PathPrefixMapping]() + @Option( + name: .customLong("compilation-db-search-path"), + parsing: .singleValue, + help: + "Specify a relative path where sourcekit-lsp should search for `compile_commands.json` or `compile_flags.txt` relative to the root of a workspace. Multiple search paths may be specified by repeating this option." + ) + var compilationDatabaseSearchPaths = [RelativePath]() + @Option( help: "Specify the directory where generated interfaces will be stored" ) @@ -157,6 +178,7 @@ struct SourceKitLSP: ParsableCommand { serverOptions.buildSetup.flags.linkerFlags = buildFlagsLinker serverOptions.buildSetup.flags.swiftCompilerFlags = buildFlagsSwift serverOptions.clangdOptions = clangdOptions + serverOptions.compilationDatabaseSearchPaths = compilationDatabaseSearchPaths serverOptions.indexOptions.indexStorePath = indexStorePath serverOptions.indexOptions.indexDatabasePath = indexDatabasePath serverOptions.indexOptions.indexPrefixMappings = indexPrefixMappings diff --git a/Tests/SKCoreTests/CompilationDatabaseTests.swift b/Tests/SKCoreTests/CompilationDatabaseTests.swift index 8638aa74..183c1284 100644 --- a/Tests/SKCoreTests/CompilationDatabaseTests.swift +++ b/Tests/SKCoreTests/CompilationDatabaseTests.swift @@ -206,7 +206,12 @@ final class CompilationDatabaseTests: XCTestCase { func testJSONCompilationDatabaseFromDirectory() throws { let fs = InMemoryFileSystem() try fs.createDirectory(AbsolutePath(validating: "/a")) - XCTAssertNil(tryLoadCompilationDatabase(directory: try AbsolutePath(validating: "/a"), fs)) + XCTAssertNil( + try tryLoadCompilationDatabase( + directory: AbsolutePath(validating: "/a"), + fs + ) + ) try fs.writeFileContents( AbsolutePath(validating: "/a/compile_commands.json"), @@ -221,13 +226,59 @@ final class CompilationDatabaseTests: XCTestCase { """ ) - XCTAssertNotNil(tryLoadCompilationDatabase(directory: try AbsolutePath(validating: "/a"), fs)) + XCTAssertNotNil( + try tryLoadCompilationDatabase( + directory: AbsolutePath(validating: "/a"), + fs + ) + ) + } + + func testJSONCompilationDatabaseFromCustomDirectory() throws { + let fs = InMemoryFileSystem() + let root = try AbsolutePath(validating: "/a") + try fs.createDirectory(root) + XCTAssertNil(tryLoadCompilationDatabase(directory: root, fs)) + + let customDir = try RelativePath(validating: "custom/build/dir") + try fs.createDirectory(root.appending(customDir), recursive: true) + + try fs.writeFileContents( + root + .appending(customDir) + .appending(component: "compile_commands.json"), + bytes: """ + [ + { + "file": "/a/a.swift", + "directory": "/a", + "arguments": ["swiftc", "/a/a.swift"] + } + ] + """ + ) + + XCTAssertNotNil( + try tryLoadCompilationDatabase( + directory: AbsolutePath(validating: "/a"), + additionalSearchPaths: [ + RelativePath(validating: "."), + customDir, + ], + fs + ) + ) } func testFixedCompilationDatabase() throws { let fs = InMemoryFileSystem() try fs.createDirectory(try AbsolutePath(validating: "/a")) - XCTAssertNil(tryLoadCompilationDatabase(directory: try AbsolutePath(validating: "/a"), fs)) + XCTAssertNil( + try tryLoadCompilationDatabase( + directory: AbsolutePath(validating: "/a"), + fs + ) + ) try fs.writeFileContents( try AbsolutePath(validating: "/a/compile_flags.txt"), @@ -238,7 +289,10 @@ final class CompilationDatabaseTests: XCTestCase { """ ) - let db = tryLoadCompilationDatabase(directory: try AbsolutePath(validating: "/a"), fs) + let db = try tryLoadCompilationDatabase( + directory: AbsolutePath(validating: "/a"), + fs + ) XCTAssertNotNil(db) XCTAssertEqual( @@ -394,6 +448,10 @@ private func checkCompilationDatabaseBuildSystem( let fs = InMemoryFileSystem() try fs.createDirectory(AbsolutePath(validating: "/a")) try fs.writeFileContents(AbsolutePath(validating: "/a/compile_commands.json"), bytes: compdb) - let buildSystem = CompilationDatabaseBuildSystem(projectRoot: try AbsolutePath(validating: "/a"), fileSystem: fs) + let buildSystem = CompilationDatabaseBuildSystem( + projectRoot: try AbsolutePath(validating: "/a"), + searchPaths: try [RelativePath(validating: ".")], + fileSystem: fs + ) try await block(buildSystem) } diff --git a/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift b/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift index edc2d682..fa8917c3 100644 --- a/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift +++ b/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift @@ -43,7 +43,11 @@ final class CompilationDatabaseTests: XCTestCase { let compilationDatabaseUrl = ws.builder.buildRoot.appendingPathComponent("compile_commands.json") _ = try ws.sources.edit({ builder in - let compilationDatabase = try JSONCompilationDatabase(file: AbsolutePath(validating: compilationDatabaseUrl.path)) + let compilationDatabase = try XCTUnwrap( + JSONCompilationDatabase( + file: AbsolutePath(validating: compilationDatabaseUrl.path) + ) + ) let newCommands = compilationDatabase.allCommands.map { (command: CompilationDatabaseCompileCommand) -> CompilationDatabaseCompileCommand in var command = command