Skip to content

Allow workspace options to affect build system search #1889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions Sources/BuildSystemIntegration/BuildSystemManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ private enum BuildSystemAdapter {
}

private extension BuildSystemSpec {
private static func createBuiltInBuildSystemAdapter(
projectRoot: URL,
private func createBuiltInBuildSystemAdapter(
messagesToSourceKitLSPHandler: any MessageHandler,
buildSystemHooks: BuildSystemHooks,
_ createBuildSystem: @Sendable (_ connectionToSourceKitLSP: any Connection) async throws -> BuiltInBuildSystem?
Expand Down Expand Up @@ -186,6 +185,7 @@ private extension BuildSystemSpec {
let buildSystem = await orLog("Creating external build system") {
try await ExternalBuildSystemAdapter(
projectRoot: projectRoot,
configPath: configPath,
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
)
}
Expand All @@ -196,23 +196,18 @@ private extension BuildSystemSpec {
logger.log("Created external build server at \(projectRoot)")
return .external(buildSystem)
case .compilationDatabase:
return await Self.createBuiltInBuildSystemAdapter(
projectRoot: projectRoot,
return await createBuiltInBuildSystemAdapter(
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
buildSystemHooks: buildSystemHooks
) { connectionToSourceKitLSP in
CompilationDatabaseBuildSystem(
projectRoot: projectRoot,
searchPaths: (options.compilationDatabaseOrDefault.searchPaths ?? []).compactMap {
try? RelativePath(validating: $0)
},
try CompilationDatabaseBuildSystem(
configPath: configPath,
connectionToSourceKitLSP: connectionToSourceKitLSP
)
}
case .swiftPM:
#if canImport(PackageModel)
return await Self.createBuiltInBuildSystemAdapter(
projectRoot: projectRoot,
return await createBuiltInBuildSystemAdapter(
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
buildSystemHooks: buildSystemHooks
) { connectionToSourceKitLSP in
Expand All @@ -228,8 +223,7 @@ private extension BuildSystemSpec {
return nil
#endif
case .injected(let injector):
return await Self.createBuiltInBuildSystemAdapter(
projectRoot: projectRoot,
return await createBuiltInBuildSystemAdapter(
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
buildSystemHooks: buildSystemHooks
) { connectionToSourceKitLSP in
Expand All @@ -248,13 +242,14 @@ package actor BuildSystemManager: QueueBasedMessageHandler {

package let messageHandlingQueue = AsyncQueue<BuildSystemMessageDependencyTracker>()

/// The root of the project that this build system manages.
/// The path to the main configuration file (or directory) that this build system manages.
///
/// For example, in SwiftPM packages this is the folder containing Package.swift.
/// For compilation databases it is the root folder based on which the compilation database was found.
/// Some examples:
/// - The path to `Package.swift` for SwiftPM packages
/// - The path to `compile_commands.json` for a JSON compilation database
///
/// `nil` if the `BuildSystemManager` does not have an underlying build system.
package let projectRoot: URL?
package let configPath: URL?

/// The files for which the delegate has requested change notifications, ie. the files for which the delegate wants to
/// get `fileBuildSettingsChanged` and `filesDependenciesUpdated` callbacks.
Expand Down Expand Up @@ -359,7 +354,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
self.toolchainRegistry = toolchainRegistry
self.options = options
self.connectionToClient = connectionToClient
self.projectRoot = buildSystemSpec?.projectRoot
self.configPath = buildSystemSpec?.configPath
self.buildSystemAdapter = await buildSystemSpec?.createBuildSystemAdapter(
toolchainRegistry: toolchainRegistry,
options: options,
Expand Down Expand Up @@ -413,6 +408,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
logger.log("Launched a legacy BSP server. Using push-based build settings model.")
let legacyBuildServer = await LegacyBuildServerBuildSystem(
projectRoot: buildSystemSpec.projectRoot,
configPath: buildSystemSpec.configPath,
initializationData: initializeResponse,
externalBuildSystemAdapter
)
Expand Down Expand Up @@ -679,8 +675,8 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
result.formUnion(targets)
}
if !filesAndDirectories.directories.isEmpty, let documentPathComponents = document.fileURL?.pathComponents {
for (directory, (directoryPathComponents, info)) in filesAndDirectories.directories {
guard let directoryPathComponents, let directoryPath = directory.fileURL else {
for (_, (directoryPathComponents, info)) in filesAndDirectories.directories {
guard let directoryPathComponents else {
continue
}
if isDescendant(documentPathComponents, of: directoryPathComponents) {
Expand Down
5 changes: 0 additions & 5 deletions Sources/BuildSystemIntegration/BuiltInBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ package struct PrepareNotSupportedError: Error, CustomStringConvertible {

/// Provider of FileBuildSettings and other build-related information.
package protocol BuiltInBuildSystem: AnyObject, Sendable {
/// The root of the project that this build system manages. For example, for SwiftPM packages, this is the folder
/// containing Package.swift. For compilation databases it is the root folder based on which the compilation database
/// was found.
var projectRoot: URL { get async }

/// The files to watch for changes.
var fileWatchers: [FileSystemWatcher] { get async }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,16 @@ package struct BuildSystemSpec {

package var kind: Kind

/// The folder that best describes the root of the project that this build system handles.
package var projectRoot: URL

package init(kind: BuildSystemSpec.Kind, projectRoot: URL) {
/// The main path that provides the build system configuration.
package var configPath: URL

package init(kind: BuildSystemSpec.Kind, projectRoot: URL, configPath: URL) {
self.kind = kind
self.projectRoot = projectRoot
self.configPath = configPath
}
}

Expand Down
47 changes: 17 additions & 30 deletions Sources/BuildSystemIntegration/CompilationDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import LanguageServerProtocolExtensions
import SKLogging
import SwiftExtensions
import TSCExtensions

package import struct TSCBasic.RelativePath
#else
import BuildServerProtocol
import Foundation
Expand All @@ -28,8 +26,6 @@ import LanguageServerProtocolExtensions
import SKLogging
import SwiftExtensions
import TSCExtensions

import struct TSCBasic.RelativePath
#endif

#if os(Windows)
Expand Down Expand Up @@ -116,33 +112,20 @@ package protocol CompilationDatabase {
var sourceItems: [SourceItem] { get }
}

/// Loads the compilation database located in `directory`, if one can be found in `additionalSearchPaths` or in the default search paths of "." and "build".
/// Loads a compilation database from `file`.
package func tryLoadCompilationDatabase(
directory: URL,
additionalSearchPaths: [RelativePath] = []
file: URL
) -> CompilationDatabase? {
let searchPaths =
additionalSearchPaths + [
// These default search paths match the behavior of `clangd`
try! RelativePath(validating: "."),
try! RelativePath(validating: "build"),
]
return
searchPaths
.lazy
.map { directory.appending($0) }
.compactMap { searchPath in
orLog("Failed to load compilation database") { () -> CompilationDatabase? in
if let compDb = try JSONCompilationDatabase(directory: searchPath) {
return compDb
}
if let compDb = try FixedCompilationDatabase(directory: searchPath) {
return compDb
}
return nil
}
orLog("Failed to load compilation database") { () -> CompilationDatabase? in
switch file.lastPathComponent {
case JSONCompilationDatabase.dbName:
return try JSONCompilationDatabase(file: file)
case FixedCompilationDatabase.dbName:
return try FixedCompilationDatabase(file: file)
default:
return nil
}
.first
}
}

/// Fixed clang-compatible compilation database (compile_flags.txt).
Expand All @@ -156,6 +139,8 @@ package func tryLoadCompilationDatabase(
///
/// See https://clang.llvm.org/docs/JSONCompilationDatabase.html under Alternatives
package struct FixedCompilationDatabase: CompilationDatabase, Equatable {
package static let dbName: String = "compile_flags.txt"

private let fixedArgs: [String]
private let directory: String

Expand All @@ -172,7 +157,7 @@ package struct FixedCompilationDatabase: CompilationDatabase, Equatable {
/// Loads the compilation database located in `directory`, if any.
/// - Returns: `nil` if `compile_flags.txt` was not found
package init?(directory: URL) throws {
let path = directory.appendingPathComponent("compile_flags.txt")
let path = directory.appendingPathComponent(Self.dbName)
try self.init(file: path)
}

Expand Down Expand Up @@ -212,6 +197,8 @@ package struct FixedCompilationDatabase: CompilationDatabase, Equatable {
///
/// See https://clang.llvm.org/docs/JSONCompilationDatabase.html
package struct JSONCompilationDatabase: CompilationDatabase, Equatable, Codable {
package static let dbName: String = "compile_commands.json"

private var pathToCommands: [DocumentURI: [Int]] = [:]
private var commands: [CompilationDatabaseCompileCommand] = []

Expand All @@ -232,7 +219,7 @@ package struct JSONCompilationDatabase: CompilationDatabase, Equatable, Codable
///
/// - Returns: `nil` if `compile_commands.json` was not found
package init?(directory: URL) throws {
let path = directory.appendingPathComponent("compile_commands.json")
let path = directory.appendingPathComponent(Self.dbName)
try self.init(file: path)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ package import LanguageServerProtocol
import LanguageServerProtocolExtensions
import SKLogging
package import SKOptions
import SwiftExtensions
import ToolchainRegistry
import TSCExtensions

package import struct TSCBasic.RelativePath
import struct TSCBasic.RelativePath
#else
import BuildServerProtocol
import Dispatch
Expand All @@ -30,6 +31,7 @@ import LanguageServerProtocol
import LanguageServerProtocolExtensions
import SKLogging
import SKOptions
import SwiftExtensions
import ToolchainRegistry
import TSCExtensions

Expand Down Expand Up @@ -59,13 +61,37 @@ fileprivate enum Cachable<Value> {
/// 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.
/// one compilation database located within the given seach paths (defaulting to the root or inside `build`).
package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem {
static package func projectRoot(for workspaceFolder: URL, options: SourceKitLSPOptions) -> URL? {
if tryLoadCompilationDatabase(directory: workspaceFolder) != nil {
return workspaceFolder
}
return nil
static package func searchForConfig(in workspaceFolder: URL, options: SourceKitLSPOptions) -> BuildSystemSpec? {
let searchPaths =
(options.compilationDatabaseOrDefault.searchPaths ?? []).compactMap {
try? RelativePath(validating: $0)
} + [
// These default search paths match the behavior of `clangd`
try! RelativePath(validating: "."),
try! RelativePath(validating: "build"),
]

return
searchPaths
.lazy
.compactMap { searchPath in
let path = workspaceFolder.appending(searchPath)

let jsonPath = path.appendingPathComponent(JSONCompilationDatabase.dbName)
if FileManager.default.isFile(at: jsonPath) {
return BuildSystemSpec(kind: .compilationDatabase, projectRoot: workspaceFolder, configPath: jsonPath)
}

let fixedPath = path.appendingPathComponent(FixedCompilationDatabase.dbName)
if FileManager.default.isFile(at: fixedPath) {
return BuildSystemSpec(kind: .compilationDatabase, projectRoot: workspaceFolder, configPath: fixedPath)
}

return nil
}
.first
}

/// The compilation database.
Expand All @@ -78,10 +104,15 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem {
}

private let connectionToSourceKitLSP: any Connection
private let searchPaths: [RelativePath]

package let projectRoot: URL
package let configPath: URL

// 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:
// The user has a build that stores `compile_commands.json` in `mybuild`. In order to pick it up, they create a
// symlink from `<project root>/compile_commands.json` to `mybuild/compile_commands.json`. We want to get notified
// about the change to `mybuild/compile_commands.json` because it effectively changes the contents of
// `<project root>/compile_commands.json`.
package let fileWatchers: [FileSystemWatcher] = [
FileSystemWatcher(globPattern: "**/compile_commands.json", kind: [.create, .change, .delete]),
FileSystemWatcher(globPattern: "**/compile_flags.txt", kind: [.create, .change, .delete]),
Expand Down Expand Up @@ -118,18 +149,17 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem {
package nonisolated var supportsPreparation: Bool { false }

package init?(
projectRoot: URL,
searchPaths: [RelativePath],
configPath: URL,
connectionToSourceKitLSP: any Connection
) {
self.projectRoot = projectRoot
self.searchPaths = searchPaths
self.connectionToSourceKitLSP = connectionToSourceKitLSP
if let compdb = tryLoadCompilationDatabase(directory: projectRoot, additionalSearchPaths: searchPaths) {
) throws {
if let compdb = tryLoadCompilationDatabase(file: configPath) {
self.compdb = compdb
} else {
return nil
}
self.connectionToSourceKitLSP = connectionToSourceKitLSP

self.configPath = configPath
}

package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse {
Expand Down Expand Up @@ -181,19 +211,13 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem {
}

private func fileEventShouldTriggerCompilationDatabaseReload(event: FileEvent) -> Bool {
switch event.uri.fileURL?.lastPathComponent {
case "compile_commands.json", "compile_flags.txt":
return true
default:
return false
}
return event.uri.fileURL?.lastPathComponent == configPath.lastPathComponent
}

/// The compilation database has been changed on disk.
/// Reload it and notify the delegate about build setting changes.
private func reloadCompilationDatabase() {
self.compdb = tryLoadCompilationDatabase(directory: projectRoot, additionalSearchPaths: searchPaths)

self.compdb = tryLoadCompilationDatabase(file: configPath)
connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil))
}
}
Loading