Files
sourcekit-lsp/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift
Alex Hoppen 022a6de6c3 Handle filesDependenciesUpdated in BuildSystemManager instead of the build system
The implementation of which file’s dependencies have been updated is common across all build systems and thus build systems shouldn’t need to implement this logic. This also allows us to remove `BuildSystemDelegate`.
2024-09-12 07:34:12 -07:00

226 lines
7.3 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import BuildServerProtocol
import Dispatch
import LanguageServerProtocol
import SKLogging
import SKOptions
import SKSupport
import ToolchainRegistry
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).
///
/// Provides build settings from a `CompilationDatabase` found by searching a project. For now, only
/// one compilation database, located at the project root.
package actor CompilationDatabaseBuildSystem {
/// The compilation database.
var compdb: CompilationDatabase? = nil {
didSet {
// Build settings have changed and thus the index store path might have changed.
// Recompute it on demand.
_indexStorePath = nil
}
}
/// Callbacks that should be called if the list of possible test files has changed.
package var testFilesDidChangeCallbacks: [() async -> Void] = []
package weak var messageHandler: BuiltInBuildSystemMessageHandler?
package let projectRoot: AbsolutePath
let searchPaths: [RelativePath]
let fileSystem: FileSystem
private var _indexStorePath: AbsolutePath?
package var indexStorePath: AbsolutePath? {
if let indexStorePath = _indexStorePath {
return indexStorePath
}
if let allCommands = self.compdb?.allCommands {
for command in allCommands {
let args = command.commandLine
for i in args.indices.reversed() {
if args[i] == "-index-store-path" && i != args.endIndex - 1 {
_indexStorePath = try? AbsolutePath(validating: args[i + 1])
return _indexStorePath
}
}
}
}
return nil
}
package init?(
projectRoot: AbsolutePath,
searchPaths: [RelativePath],
messageHandler: (any BuiltInBuildSystemMessageHandler)?,
fileSystem: FileSystem = localFileSystem
) {
self.fileSystem = fileSystem
self.projectRoot = projectRoot
self.searchPaths = searchPaths
self.messageHandler = messageHandler
if let compdb = tryLoadCompilationDatabase(directory: projectRoot, additionalSearchPaths: searchPaths, fileSystem) {
self.compdb = compdb
} else {
return nil
}
}
}
extension CompilationDatabaseBuildSystem: BuiltInBuildSystem {
static package func projectRoot(for workspaceFolder: AbsolutePath, options: SourceKitLSPOptions) -> AbsolutePath? {
if tryLoadCompilationDatabase(directory: workspaceFolder) != nil {
return workspaceFolder
}
return nil
}
package nonisolated var supportsPreparation: Bool { false }
package var indexDatabasePath: AbsolutePath? {
indexStorePath?.parentDirectory.appending(component: "IndexDatabase")
}
package func buildTargets(request: BuildTargetsRequest) async throws -> BuildTargetsResponse {
return BuildTargetsResponse(targets: [
BuildTarget(
id: .dummy,
displayName: nil,
baseDirectory: nil,
tags: [.test],
capabilities: BuildTargetCapabilities(),
// Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property.
languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift],
dependencies: []
)
])
}
package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse {
guard request.targets.contains(.dummy) else {
return BuildTargetSourcesResponse(items: [])
}
guard let compdb else {
return BuildTargetSourcesResponse(items: [])
}
let sources = compdb.allCommands.map {
SourceItem(uri: $0.uri, kind: .file, generated: false)
}
return BuildTargetSourcesResponse(items: [SourcesItem(target: .dummy, sources: sources)])
}
package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async {
if notification.changes.contains(where: { self.fileEventShouldTriggerCompilationDatabaseReload(event: $0) }) {
await self.reloadCompilationDatabase()
}
}
package func inverseSources(request: InverseSourcesRequest) -> InverseSourcesResponse {
return InverseSourcesResponse(targets: [BuildTargetIdentifier.dummy])
}
package func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse {
throw PrepareNotSupportedError()
}
package func sourceKitOptions(request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse? {
guard let db = database(for: request.textDocument.uri), let cmd = db[request.textDocument.uri].first else {
return nil
}
return SourceKitOptionsResponse(
compilerArguments: Array(cmd.commandLine.dropFirst()),
workingDirectory: cmd.directory
)
}
package func toolchain(for uri: DocumentURI, _ language: Language) async -> Toolchain? {
return nil
}
package func scheduleBuildGraphGeneration() {}
package func waitForUpToDateBuildGraph() async {}
package func topologicalSort(of targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
return nil
}
package func targets(dependingOn targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
return nil
}
private func database(for uri: DocumentURI) -> CompilationDatabase? {
if let url = uri.fileURL, let path = try? AbsolutePath(validating: url.path) {
return database(for: path)
}
return compdb
}
private func database(for path: AbsolutePath) -> CompilationDatabase? {
if compdb == nil {
var dir = path
while !dir.isRoot {
dir = dir.parentDirectory
if let db = tryLoadCompilationDatabase(directory: dir, additionalSearchPaths: searchPaths, fileSystem) {
compdb = db
break
}
}
}
if compdb == nil {
logger.error("Could not open compilation database for \(path)")
}
return compdb
}
private func fileEventShouldTriggerCompilationDatabaseReload(event: FileEvent) -> Bool {
switch event.uri.fileURL?.lastPathComponent {
case "compile_commands.json", "compile_flags.txt":
return true
default:
return false
}
}
/// The compilation database has been changed on disk.
/// Reload it and notify the delegate about build setting changes.
private func reloadCompilationDatabase() async {
self.compdb = tryLoadCompilationDatabase(
directory: projectRoot,
additionalSearchPaths: searchPaths,
self.fileSystem
)
await messageHandler?.sendNotificationToSourceKitLSP(DidChangeBuildTargetNotification(changes: nil))
for testFilesDidChangeCallback in testFilesDidChangeCallbacks {
await testFilesDidChangeCallback()
}
}
package func addSourceFilesDidChangeCallback(_ callback: @escaping () async -> Void) async {
testFilesDidChangeCallbacks.append(callback)
}
}