File: JSONCompilationDatabaseBuildSystem.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (198 lines) | stat: -rw-r--r-- 8,047 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

package import BuildServerProtocol
package import Foundation
package import LanguageServerProtocol
import SKLogging
import SwiftExtensions
package import ToolchainRegistry

fileprivate extension CompilationDatabaseCompileCommand {
  /// The first entry in the command line identifies the compiler that should be used to compile the file and can thus
  /// be used to infer the toolchain.
  ///
  /// Note that this compiler does not necessarily need to exist on disk. Eg. tools may just use `clang` as the compiler
  /// without specifying a path.
  ///
  /// The absence of a compiler means we have an empty command line, which should never happen.
  ///
  /// If the compiler is a symlink to `swiftly`, it uses `swiftlyResolver` to find the corresponding executable in a
  /// real toolchain and returns that executable.
  func compiler(swiftlyResolver: SwiftlyResolver) async -> String? {
    guard let compiler = commandLine.first else {
      return nil
    }
    let swiftlyResolved = await orLog("Resolving swiftly") {
      try await swiftlyResolver.resolve(
        compiler: URL(fileURLWithPath: compiler),
        workingDirectory: URL(fileURLWithPath: directory)
      )?.filePath
    }
    if let swiftlyResolved {
      return swiftlyResolved
    }
    return compiler
  }
}

/// A `BuildSystem` that provides compiler arguments from a `compile_commands.json` file.
package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
  package static let dbName: String = "compile_commands.json"

  /// The compilation database.
  var compdb: JSONCompilationDatabase {
    didSet {
      // Build settings have changed and thus the index store path might have changed.
      // Recompute it on demand.
      _indexStorePath.reset()
    }
  }

  private let toolchainRegistry: ToolchainRegistry

  private let connectionToSourceKitLSP: any Connection

  package let configPath: URL

  private let swiftlyResolver = SwiftlyResolver()

  // 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: "**/.swift-version", kind: [.create, .change, .delete]),
  ]

  private var _indexStorePath: LazyValue<URL?> = .uninitialized
  package var indexStorePath: URL? {
    _indexStorePath.cachedValueOrCompute {
      for command in compdb.commands {
        if let indexStorePath = lastIndexStorePathArgument(in: command.commandLine) {
          return URL(
            fileURLWithPath: indexStorePath,
            relativeTo: URL(fileURLWithPath: command.directory, isDirectory: true)
          )
        }
      }
      return nil
    }
  }

  package var indexDatabasePath: URL? {
    indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase")
  }

  package nonisolated var supportsPreparationAndOutputPaths: Bool { false }

  package init(
    configPath: URL,
    toolchainRegistry: ToolchainRegistry,
    connectionToSourceKitLSP: any Connection
  ) throws {
    self.compdb = try JSONCompilationDatabase(file: configPath)
    self.toolchainRegistry = toolchainRegistry
    self.connectionToSourceKitLSP = connectionToSourceKitLSP
    self.configPath = configPath
  }

  package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse {
    let compilers = Set(
      await compdb.commands.asyncCompactMap { (command) -> String? in
        await command.compiler(swiftlyResolver: swiftlyResolver)
      }
    ).sorted { $0 < $1 }
    let targets = try await compilers.asyncMap { compiler in
      let toolchainUri: URI? =
        if let toolchainPath = await toolchainRegistry.toolchain(withCompiler: URL(fileURLWithPath: compiler))?.path {
          URI(toolchainPath)
        } else {
          nil
        }
      return BuildTarget(
        id: try BuildTargetIdentifier.createCompileCommands(compiler: compiler),
        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: [],
        dataKind: .sourceKit,
        data: SourceKitBuildTarget(toolchain: toolchainUri).encodeToLSPAny()
      )
    }
    return WorkspaceBuildTargetsResponse(targets: targets)
  }

  package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse {
    let items = await request.targets.asyncCompactMap { (target) -> SourcesItem? in
      guard let targetCompiler = orLog("Compiler for target", { try target.compileCommandsCompiler }) else {
        return nil
      }
      let commandsWithRequestedCompilers = await compdb.commands.lazy.asyncFilter { command in
        return await targetCompiler == command.compiler(swiftlyResolver: swiftlyResolver)
      }
      let sources = commandsWithRequestedCompilers.map {
        SourceItem(uri: $0.uri, kind: .file, generated: false)
      }
      return SourcesItem(target: target, sources: Array(sources))
    }

    return BuildTargetSourcesResponse(items: items)
  }

  package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) async {
    if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == Self.dbName }) {
      self.reloadCompilationDatabase()
    }
    if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == ".swift-version" }) {
      await swiftlyResolver.clearCache()
      connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil))
    }
  }

  package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse {
    throw ResponseError.methodNotFound(BuildTargetPrepareRequest.method)
  }

  package func sourceKitOptions(
    request: TextDocumentSourceKitOptionsRequest
  ) async throws -> TextDocumentSourceKitOptionsResponse? {
    let targetCompiler = try request.target.compileCommandsCompiler
    let command = await compdb[request.textDocument.uri].asyncFilter {
      return await $0.compiler(swiftlyResolver: swiftlyResolver) == targetCompiler
    }.first
    guard let command else {
      return nil
    }
    return TextDocumentSourceKitOptionsResponse(
      compilerArguments: Array(command.commandLine.dropFirst()),
      workingDirectory: command.directory
    )
  }

  package func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse {
    return VoidResponse()
  }

  /// The compilation database has been changed on disk.
  /// Reload it and notify the delegate about build setting changes.
  private func reloadCompilationDatabase() {
    orLog("Reloading compilation database") {
      self.compdb = try JSONCompilationDatabase(file: configPath)
      connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil))
    }
  }
}