Files
sourcekit-lsp/Sources/SKCore/BuildServerBuildSystem.swift
2019-10-04 14:51:26 -07:00

268 lines
9.0 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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 TSCBasic
import LanguageServerProtocol
import LanguageServerProtocolJSONRPC
import SKSupport
import Foundation
import BuildServerProtocol
typealias Notification = LanguageServerProtocol.Notification
/// A `BuildSystem` based on communicating with a build server
///
/// Provides build settings from a build server launched based on a
/// `buildServer.json` configuration file provided in the repo root.
public final class BuildServerBuildSystem {
let projectRoot: AbsolutePath
let buildFolder: AbsolutePath?
let serverConfig: BuildServerConfig
let requestQueue: DispatchQueue
var handler: BuildServerHandler?
var buildServer: Connection?
public private(set) var indexStorePath: AbsolutePath?
/// Delegate to handle any build system events.
public weak var delegate: BuildSystemDelegate? {
get { return self.handler?.delegate }
set { self.handler?.delegate = newValue }
}
public init(projectRoot: AbsolutePath, buildFolder: AbsolutePath?, fileSystem: FileSystem = localFileSystem) throws {
let configPath = projectRoot.appending(component: "buildServer.json")
let config = try loadBuildServerConfig(path: configPath, fileSystem: fileSystem)
self.buildFolder = buildFolder
self.projectRoot = projectRoot
self.requestQueue = DispatchQueue(label: "build_server_request_queue")
self.serverConfig = config
try self.initializeBuildServer()
}
/// Creates a build system using the Build Server Protocol config.
///
/// - Returns: nil if `projectRoot` has no config or there is an error parsing it.
public convenience init?(projectRoot: AbsolutePath?, buildSetup: BuildSetup)
{
if projectRoot == nil { return nil }
do {
try self.init(projectRoot: projectRoot!, buildFolder: buildSetup.path)
} catch _ as FileSystemError {
// config file was missing, no build server for this workspace
return nil
} catch {
log("failed to start build server: \(error)", level: .error)
return nil
}
}
deinit {
if let buildServer = self.buildServer {
_ = buildServer.send(ShutdownBuild(), queue: DispatchQueue.global(), reply: { result in
if let error = result.failure {
log("error shutting down build server: \(error)")
}
buildServer.send(ExitBuildNotification())
})
}
}
private func initializeBuildServer() throws {
let serverPath = AbsolutePath(serverConfig.argv[0], relativeTo: projectRoot)
let flags = Array(serverConfig.argv[1...])
let languages = [
Language.c,
Language.cpp,
Language.objective_c,
Language.objective_cpp,
Language.swift,
]
let initializeRequest = InitializeBuild(
displayName: "SourceKit-LSP",
version: "1.0",
bspVersion: "2.0",
rootUri: self.projectRoot.asURL,
capabilities: BuildClientCapabilities(languageIds: languages))
let handler = BuildServerHandler()
let buildServer = try makeJSONRPCBuildServer(client: handler, serverPath: serverPath, serverFlags: flags)
let response = try buildServer.sendSync(initializeRequest)
buildServer.send(InitializedBuildNotification())
log("initialized build server \(response.displayName)")
// see if index store was set as part of the server metadata
if let indexStorePath = readReponseDataKey(data: response.data, key: "indexStorePath") {
self.indexStorePath = AbsolutePath(indexStorePath, relativeTo: self.projectRoot)
}
self.buildServer = buildServer
self.handler = handler
}
}
private func readReponseDataKey(data: LSPAny?, key: String) -> String? {
if case .dictionary(let dataDict)? = data,
case .string(let stringVal)? = dataDict[key] {
return stringVal
}
return nil
}
final class BuildServerHandler: LanguageServerEndpoint {
public weak var delegate: BuildSystemDelegate? = nil
override func _registerBuiltinHandlers() {
_register(BuildServerHandler.handleFileOptionsChanged)
}
func handleFileOptionsChanged(_ notification: Notification<FileOptionsChangedNotification>) {
// TODO: add delegate method to include the changed settings directly
self.delegate?.fileBuildSettingsChanged([notification.params.uri])
}
}
extension BuildServerBuildSystem: BuildSystem {
/// Register the given file for build-system level change notifications, such as command
/// line flag changes, dependency changes, etc.
public func registerForChangeNotifications(for url: LanguageServerProtocol.URL) {
let request = RegisterForChanges(uri: url, action: .register)
_ = self.buildServer?.send(request, queue: requestQueue, reply: { result in
if let error = result.failure {
log("error registering \(url): \(error)", level: .error)
}
})
}
/// Unregister the given file for build-system level change notifications, such as command
/// line flag changes, dependency changes, etc.
public func unregisterForChangeNotifications(for url: LanguageServerProtocol.URL) {
let request = RegisterForChanges(uri: url, action: .unregister)
_ = self.buildServer?.send(request, queue: requestQueue, reply: { result in
if let error = result.failure {
log("error unregistering \(url): \(error)", level: .error)
}
})
}
public var indexDatabasePath: AbsolutePath? {
return buildFolder?.appending(components: "index", "db")
}
public func settings(for url: URL, _ language: Language) -> FileBuildSettings? {
if let response = try? self.buildServer?.sendSync(SourceKitOptions(uri: url)) {
return FileBuildSettings(compilerArguments: response.options, workingDirectory: response.workingDirectory)
}
return nil
}
public func toolchain(for: URL, _ language: Language) -> Toolchain? {
return nil
}
public func buildTargets(reply: @escaping (LSPResult<[BuildTarget]>) -> Void) {
_ = self.buildServer?.send(BuildTargets(), queue: requestQueue) { response in
switch response {
case .success(let result):
reply(.success(result.targets))
case .failure(let error):
reply(.failure(error))
}
}
}
public func buildTargetSources(targets: [BuildTargetIdentifier], reply: @escaping ([SourcesItem]?) -> Void) {
let req = BuildTargetSources(targets: targets)
_ = self.buildServer?.send(req, queue: requestQueue) { result in
if let items = result.success?.items { reply(items) }
else {
log("error fetching build target sources: \(result)")
reply(nil)
}
}
}
public func buildTargetOutputPaths(targets: [BuildTargetIdentifier], reply: @escaping ([OutputsItem]?) -> Void) {
let req = BuildTargetOutputPaths(targets: targets)
_ = self.buildServer?.send(req, queue: requestQueue) { result in
if let items = result.success?.items { reply(items) }
else {
log("error fetching build target outputs: \(result)")
reply(nil)
}
}
}
}
private func loadBuildServerConfig(path: AbsolutePath, fileSystem: FileSystem) throws -> BuildServerConfig {
let decoder = JSONDecoder()
let fileData = try fileSystem.readFileContents(path).contents
return try decoder.decode(BuildServerConfig.self, from: Data(fileData))
}
struct BuildServerConfig: Codable {
/// The name of the build tool.
let name: String
/// The version of the build tool.
let version: String
/// The bsp version of the build tool.
let bspVersion: String
/// A collection of languages supported by this BSP server.
let languages: [String]
/// Command arguments runnable via system processes to start a BSP server.
let argv: [String]
}
private func makeJSONRPCBuildServer(client: MessageHandler, serverPath: AbsolutePath, serverFlags: [String]?) throws -> Connection {
let clientToServer = Pipe()
let serverToClient = Pipe()
let connection = JSONRPCConection(
protocol: BuildServerProtocol.bspRegistry,
inFD: serverToClient.fileHandleForReading.fileDescriptor,
outFD: clientToServer.fileHandleForWriting.fileDescriptor
)
connection.start(receiveHandler: client)
let process = Foundation.Process()
if #available(OSX 10.13, *) {
process.executableURL = serverPath.asURL
} else {
process.launchPath = serverPath.pathString
}
process.arguments = serverFlags
process.standardOutput = serverToClient
process.standardInput = clientToServer
process.terminationHandler = { process in
log("build server exited: \(process.terminationReason) \(process.terminationStatus)")
connection.close()
}
if #available(OSX 10.13, *) {
try process.run()
} else {
process.launch()
}
return connection
}