Watch for changes to compile_commands.json and compile_flags.txt

Similar to how we reload the package if Package.swift is changed, we also watch `compilation_database.json` and `compile_flags.txt` and notify the build system delegate that build settings changed if these files are modified.

rdar://92388223  [#486]
This commit is contained in:
Alex Hoppen
2022-04-28 10:28:41 +02:00
parent a4760bfd12
commit 05a9237f2c
3 changed files with 183 additions and 19 deletions

View File

@@ -24,30 +24,58 @@ import struct Foundation.URL
/// one compilation database, located at the project root.
public final class CompilationDatabaseBuildSystem {
/// Queue guarding the following properties:
/// - `compdb`
/// - `watchedFiles`
/// - `_indexStorePath`
let queue: DispatchQueue = .init(label: "CompilationDatabaseBuildSystem.queue", qos: .userInitiated)
/// The compilation database.
var compdb: CompilationDatabase? = nil
var compdb: CompilationDatabase? = nil {
didSet {
dispatchPrecondition(condition: .onQueue(queue))
// Build settings have changed and thus the index store path might have changed.
// Recompute it on demand.
_indexStorePath = nil
}
}
/// Delegate to handle any build system events.
public weak var delegate: BuildSystemDelegate? = nil
let projectRoot: AbsolutePath?
let fileSystem: FileSystem
public lazy var indexStorePath: AbsolutePath? = {
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 {
return try? AbsolutePath(validating: args[i+1])
/// The URIs for which the delegate has registered for change notifications,
/// mapped to the language the delegate specified when registering for change notifications.
var watchedFiles: [DocumentURI: Language] = [:]
private var _indexStorePath: AbsolutePath?
public var indexStorePath: AbsolutePath? {
return queue.sync {
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
}
return nil
}()
}
public init(projectRoot: AbsolutePath? = nil, fileSystem: FileSystem = localFileSystem) {
self.fileSystem = fileSystem
self.projectRoot = projectRoot
if let path = projectRoot {
self.compdb = tryLoadCompilationDatabase(directory: path, fileSystem)
}
@@ -61,16 +89,22 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
}
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) {
guard let delegate = self.delegate else { return }
queue.async {
self.watchedFiles[uri] = language
let settings = self._settings(for: uri)
DispatchQueue.global().async {
guard let delegate = self.delegate else { return }
let settings = self.settings(for: uri)
delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
}
}
/// We don't support change watching.
public func unregisterForChangeNotifications(for: DocumentURI) {}
public func unregisterForChangeNotifications(for uri: DocumentURI) {
queue.async {
self.watchedFiles[uri] = nil
}
}
public func buildTargets(reply: @escaping (LSPResult<[BuildTarget]>) -> Void) {
reply(.failure(buildTargetsNotSupported))
@@ -84,14 +118,18 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
reply(.failure(buildTargetsNotSupported))
}
func database(for url: URL) -> CompilationDatabase? {
/// Must be invoked on `queue`.
private func database(for url: URL) -> CompilationDatabase? {
dispatchPrecondition(condition: .onQueue(queue))
if let path = try? AbsolutePath(validating: url.path) {
return database(for: path)
}
return compdb
}
func database(for path: AbsolutePath) -> CompilationDatabase? {
/// Must be invoked on `queue`.
private func database(for path: AbsolutePath) -> CompilationDatabase? {
dispatchPrecondition(condition: .onQueue(queue))
if compdb == nil {
var dir = path
while !dir.isRoot {
@@ -110,12 +148,51 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
return compdb
}
public func filesDidChange(_ events: [FileEvent]) {}
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.
/// Must be called on `queue`.
private func reloadCompilationDatabase() {
dispatchPrecondition(condition: .onQueue(queue))
guard let projectRoot = self.projectRoot else { return }
self.compdb = tryLoadCompilationDatabase(directory: projectRoot, self.fileSystem)
if let delegate = self.delegate {
var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:]
for (uri, _) in self.watchedFiles {
if let settings = self.settings(for: uri) {
changedFiles[uri] = FileBuildSettingsChange(settings)
} else {
changedFiles[uri] = .removedOrUnavailable
}
}
delegate.fileBuildSettingsChanged(changedFiles)
}
}
public func filesDidChange(_ events: [FileEvent]) {
queue.async {
if events.contains(where: { self.fileEventShouldTriggerCompilationDatabaseReload(event: $0) }) {
self.reloadCompilationDatabase()
}
}
}
}
extension CompilationDatabaseBuildSystem {
/// Exposed for *testing*.
public func _settings(for uri: DocumentURI) -> FileBuildSettings? {
/// Must be invoked on `queue`.
private func settings(for uri: DocumentURI) -> FileBuildSettings? {
dispatchPrecondition(condition: .onQueue(queue))
guard let url = uri.fileURL else {
// We can't determine build settings for non-file URIs.
return nil
@@ -126,4 +203,11 @@ extension CompilationDatabaseBuildSystem {
compilerArguments: Array(cmd.commandLine.dropFirst()),
workingDirectory: cmd.directory)
}
/// Exposed for *testing*.
public func _settings(for uri: DocumentURI) -> FileBuildSettings? {
return queue.sync {
return self.settings(for: uri)
}
}
}