mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
This allows us to express that `body` will run on the same actor isolation domain as the caller of `orLog`, which effectively makes `orLog` usable from actors again.
473 lines
16 KiB
Swift
473 lines
16 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 Basics
|
|
import Build
|
|
import BuildServerProtocol
|
|
import Dispatch
|
|
import LSPLogging
|
|
import LanguageServerProtocol
|
|
import PackageGraph
|
|
import PackageLoading
|
|
import PackageModel
|
|
import SKCore
|
|
import SKSupport
|
|
import SourceControl
|
|
import SourceKitLSPAPI
|
|
import Workspace
|
|
|
|
import struct Basics.AbsolutePath
|
|
import struct Basics.TSCAbsolutePath
|
|
import struct Foundation.URL
|
|
import protocol TSCBasic.FileSystem
|
|
import var TSCBasic.localFileSystem
|
|
import func TSCBasic.resolveSymlinks
|
|
|
|
#if canImport(SPMBuildCore)
|
|
import SPMBuildCore
|
|
#endif
|
|
|
|
/// Parameter of `reloadPackageStatusCallback` in ``SwiftPMWorkspace``.
|
|
///
|
|
/// Informs the callback about whether `reloadPackage` started or finished executing.
|
|
public enum ReloadPackageStatus: Sendable {
|
|
case start
|
|
case end
|
|
}
|
|
|
|
/// A build target in SwiftPM
|
|
public typealias SwiftBuildTarget = SourceKitLSPAPI.BuildTarget
|
|
|
|
/// A build target in `BuildServerProtocol`
|
|
public typealias BuildServerTarget = BuildServerProtocol.BuildTarget
|
|
|
|
/// Same as `toolchainRegistry.default`.
|
|
///
|
|
/// Needed to work around a compiler crash that prevents us from accessing `toolchainRegistry.default` in
|
|
/// `SwiftPMWorkspace.init`.
|
|
private func getDefaultToolchain(_ toolchainRegistry: ToolchainRegistry) async -> SKCore.Toolchain? {
|
|
return await toolchainRegistry.default
|
|
}
|
|
|
|
/// Swift Package Manager build system and workspace support.
|
|
///
|
|
/// This class implements the `BuildSystem` interface to provide the build settings for a Swift
|
|
/// Package Manager (SwiftPM) package. The settings are determined by loading the Package.swift
|
|
/// manifest using `libSwiftPM` and constructing a build plan using the default (debug) parameters.
|
|
public actor SwiftPMBuildSystem {
|
|
|
|
public enum Error: Swift.Error {
|
|
|
|
/// Could not find a manifest (Package.swift file). This is not a package.
|
|
case noManifest(workspacePath: TSCAbsolutePath)
|
|
|
|
/// Could not determine an appropriate toolchain for swiftpm to use for manifest loading.
|
|
case cannotDetermineHostToolchain
|
|
}
|
|
|
|
/// Delegate to handle any build system events.
|
|
public weak var delegate: SKCore.BuildSystemDelegate? = nil
|
|
|
|
public func setDelegate(_ delegate: SKCore.BuildSystemDelegate?) async {
|
|
self.delegate = delegate
|
|
}
|
|
|
|
let workspacePath: TSCAbsolutePath
|
|
/// The directory containing `Package.swift`.
|
|
public var projectRoot: TSCAbsolutePath
|
|
var modulesGraph: ModulesGraph
|
|
let workspace: Workspace
|
|
public let buildParameters: BuildParameters
|
|
let fileSystem: FileSystem
|
|
|
|
var fileToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
|
|
var sourceDirToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
|
|
|
|
/// 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: Set<DocumentURI> = []
|
|
|
|
/// This callback is informed when `reloadPackage` starts and ends executing.
|
|
var reloadPackageStatusCallback: (ReloadPackageStatus) async -> Void
|
|
|
|
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
|
|
///
|
|
/// - Parameters:
|
|
/// - workspace: The workspace root path.
|
|
/// - toolchainRegistry: The toolchain registry to use to provide the Swift compiler used for
|
|
/// manifest parsing and runtime support.
|
|
/// - reloadPackageStatusCallback: Will be informed when `reloadPackage` starts and ends executing.
|
|
/// - Throws: If there is an error loading the package, or no manifest is found.
|
|
public init(
|
|
workspacePath: TSCAbsolutePath,
|
|
toolchainRegistry: ToolchainRegistry,
|
|
fileSystem: FileSystem = localFileSystem,
|
|
buildSetup: BuildSetup,
|
|
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void = { _ in }
|
|
) async throws {
|
|
self.workspacePath = workspacePath
|
|
self.fileSystem = fileSystem
|
|
|
|
guard let packageRoot = findPackageDirectory(containing: workspacePath, fileSystem) else {
|
|
throw Error.noManifest(workspacePath: workspacePath)
|
|
}
|
|
|
|
self.projectRoot = try resolveSymlinks(packageRoot)
|
|
|
|
guard let destinationToolchainBinDir = await getDefaultToolchain(toolchainRegistry)?.swiftc?.parentDirectory else {
|
|
throw Error.cannotDetermineHostToolchain
|
|
}
|
|
|
|
let swiftSDK = try SwiftSDK.hostSwiftSDK(AbsolutePath(destinationToolchainBinDir))
|
|
let toolchain = try UserToolchain(swiftSDK: swiftSDK)
|
|
|
|
var location = try Workspace.Location(
|
|
forRootPackage: AbsolutePath(packageRoot),
|
|
fileSystem: fileSystem
|
|
)
|
|
if let scratchDirectory = buildSetup.path {
|
|
location.scratchDirectory = AbsolutePath(scratchDirectory)
|
|
}
|
|
|
|
var configuration = WorkspaceConfiguration.default
|
|
configuration.skipDependenciesUpdates = true
|
|
|
|
self.workspace = try Workspace(
|
|
fileSystem: fileSystem,
|
|
location: location,
|
|
configuration: configuration,
|
|
customHostToolchain: toolchain
|
|
)
|
|
|
|
let buildConfiguration: PackageModel.BuildConfiguration
|
|
switch buildSetup.configuration {
|
|
case .debug, nil:
|
|
buildConfiguration = .debug
|
|
case .release:
|
|
buildConfiguration = .release
|
|
}
|
|
|
|
self.buildParameters = try BuildParameters(
|
|
dataPath: location.scratchDirectory.appending(component: toolchain.targetTriple.platformBuildPathComponent),
|
|
configuration: buildConfiguration,
|
|
toolchain: toolchain,
|
|
flags: buildSetup.flags
|
|
)
|
|
|
|
self.modulesGraph = try ModulesGraph(rootPackages: [], dependencies: [], binaryArtifacts: [:])
|
|
self.reloadPackageStatusCallback = reloadPackageStatusCallback
|
|
|
|
try await reloadPackage()
|
|
}
|
|
|
|
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
|
|
///
|
|
/// - Parameters:
|
|
/// - reloadPackageStatusCallback: Will be informed when `reloadPackage` starts and ends executing.
|
|
/// - Returns: nil if `workspacePath` is not part of a package or there is an error.
|
|
public init?(
|
|
url: URL,
|
|
toolchainRegistry: ToolchainRegistry,
|
|
buildSetup: BuildSetup,
|
|
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void
|
|
) async {
|
|
do {
|
|
try await self.init(
|
|
workspacePath: try TSCAbsolutePath(validating: url.path),
|
|
toolchainRegistry: toolchainRegistry,
|
|
fileSystem: localFileSystem,
|
|
buildSetup: buildSetup,
|
|
reloadPackageStatusCallback: reloadPackageStatusCallback
|
|
)
|
|
} catch Error.noManifest {
|
|
return nil
|
|
} catch {
|
|
logger.error("failed to create SwiftPMWorkspace at \(url.path): \(error.forLogging)")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SwiftPMBuildSystem {
|
|
|
|
/// (Re-)load the package settings by parsing the manifest and resolving all the targets and
|
|
/// dependencies.
|
|
func reloadPackage() async throws {
|
|
await reloadPackageStatusCallback(.start)
|
|
defer {
|
|
Task {
|
|
await reloadPackageStatusCallback(.end)
|
|
}
|
|
}
|
|
|
|
let observabilitySystem = ObservabilitySystem({ scope, diagnostic in
|
|
logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)")
|
|
})
|
|
|
|
let modulesGraph = try self.workspace.loadPackageGraph(
|
|
rootInput: PackageGraphRootInput(packages: [AbsolutePath(projectRoot)]),
|
|
forceResolvedVersions: true,
|
|
availableLibraries: self.buildParameters.toolchain.providedLibraries,
|
|
observabilityScope: observabilitySystem.topScope
|
|
)
|
|
|
|
let plan = try BuildPlan(
|
|
productsBuildParameters: buildParameters,
|
|
toolsBuildParameters: buildParameters,
|
|
graph: modulesGraph,
|
|
fileSystem: fileSystem,
|
|
observabilityScope: observabilitySystem.topScope
|
|
)
|
|
let buildDescription = BuildDescription(buildPlan: plan)
|
|
|
|
/// Make sure to execute any throwing statements before setting any
|
|
/// properties because otherwise we might end up in an inconsistent state
|
|
/// with only some properties modified.
|
|
self.modulesGraph = modulesGraph
|
|
|
|
self.fileToTarget = [AbsolutePath: SwiftBuildTarget](
|
|
modulesGraph.allTargets.flatMap { target in
|
|
return target.sources.paths.compactMap {
|
|
guard let buildTarget = buildDescription.getBuildTarget(for: target) else {
|
|
return nil
|
|
}
|
|
return (key: $0, value: buildTarget)
|
|
}
|
|
},
|
|
uniquingKeysWith: { td, _ in
|
|
// FIXME: is there a preferred target?
|
|
return td
|
|
}
|
|
)
|
|
|
|
self.sourceDirToTarget = [AbsolutePath: SwiftBuildTarget](
|
|
modulesGraph.allTargets.compactMap { (target) -> (AbsolutePath, SwiftBuildTarget)? in
|
|
guard let buildTarget = buildDescription.getBuildTarget(for: target) else {
|
|
return nil
|
|
}
|
|
return (key: target.sources.root, value: buildTarget)
|
|
},
|
|
uniquingKeysWith: { td, _ in
|
|
// FIXME: is there a preferred target?
|
|
return td
|
|
}
|
|
)
|
|
|
|
guard let delegate = self.delegate else {
|
|
return
|
|
}
|
|
await delegate.fileBuildSettingsChanged(self.watchedFiles)
|
|
await delegate.fileHandlingCapabilityChanged()
|
|
}
|
|
}
|
|
|
|
extension SwiftPMBuildSystem: SKCore.BuildSystem {
|
|
|
|
public var buildPath: TSCAbsolutePath {
|
|
return TSCAbsolutePath(buildParameters.buildPath)
|
|
}
|
|
|
|
public var indexStorePath: TSCAbsolutePath? {
|
|
return buildParameters.indexStoreMode == .off ? nil : TSCAbsolutePath(buildParameters.indexStore)
|
|
}
|
|
|
|
public var indexDatabasePath: TSCAbsolutePath? {
|
|
return buildPath.appending(components: "index", "db")
|
|
}
|
|
|
|
public var indexPrefixMappings: [PathPrefixMapping] { return [] }
|
|
|
|
public func buildSettings(for uri: DocumentURI, language: Language) throws -> FileBuildSettings? {
|
|
guard let url = uri.fileURL else {
|
|
// We can't determine build settings for non-file URIs.
|
|
return nil
|
|
}
|
|
guard let path = try? AbsolutePath(validating: url.path) else {
|
|
return nil
|
|
}
|
|
|
|
if let buildTarget = try buildTarget(for: path) {
|
|
return FileBuildSettings(
|
|
compilerArguments: try buildTarget.compileArguments(for: path.asURL),
|
|
workingDirectory: workspacePath.pathString
|
|
)
|
|
}
|
|
|
|
if path.basename == "Package.swift" {
|
|
return try settings(forPackageManifest: path)
|
|
}
|
|
|
|
if path.extension == "h" {
|
|
return try settings(forHeader: path, language)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
public func registerForChangeNotifications(for uri: DocumentURI) async {
|
|
self.watchedFiles.insert(uri)
|
|
}
|
|
|
|
/// Unregister the given file for build-system level change notifications, such as command
|
|
/// line flag changes, dependency changes, etc.
|
|
public func unregisterForChangeNotifications(for uri: DocumentURI) {
|
|
self.watchedFiles.remove(uri)
|
|
}
|
|
|
|
/// Returns the resolved target description for the given file, if one is known.
|
|
private func buildTarget(for file: AbsolutePath) throws -> SwiftBuildTarget? {
|
|
if let td = fileToTarget[file] {
|
|
return td
|
|
}
|
|
|
|
let realpath = try resolveSymlinks(file)
|
|
if realpath != file, let td = fileToTarget[realpath] {
|
|
fileToTarget[file] = td
|
|
return td
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// An event is relevant if it modifies a file that matches one of the file rules used by the SwiftPM workspace.
|
|
private func fileEventShouldTriggerPackageReload(event: FileEvent) -> Bool {
|
|
guard let fileURL = event.uri.fileURL else {
|
|
return false
|
|
}
|
|
switch event.type {
|
|
case .created, .deleted:
|
|
guard let path = try? AbsolutePath(validating: fileURL.path) else {
|
|
return false
|
|
}
|
|
|
|
return self.workspace.fileAffectsSwiftOrClangBuildSettings(
|
|
filePath: path,
|
|
packageGraph: self.modulesGraph
|
|
)
|
|
case .changed:
|
|
return fileURL.lastPathComponent == "Package.swift"
|
|
default: // Unknown file change type
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func filesDidChange(_ events: [FileEvent]) async {
|
|
if events.contains(where: { self.fileEventShouldTriggerPackageReload(event: $0) }) {
|
|
logger.log("Reloading package because of file change")
|
|
await orLog("Reloading package") {
|
|
// TODO: It should not be necessary to reload the entire package just to get build settings for one file.
|
|
try await self.reloadPackage()
|
|
}
|
|
}
|
|
}
|
|
|
|
public func fileHandlingCapability(for uri: DocumentURI) -> FileHandlingCapability {
|
|
guard let fileUrl = uri.fileURL else {
|
|
return .unhandled
|
|
}
|
|
if (try? buildTarget(for: AbsolutePath(validating: fileUrl.path))) != nil {
|
|
return .handled
|
|
} else {
|
|
return .unhandled
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SwiftPMBuildSystem {
|
|
|
|
// MARK: Implementation details
|
|
|
|
/// Retrieve settings for a package manifest (Package.swift).
|
|
private func settings(forPackageManifest path: AbsolutePath) throws -> FileBuildSettings? {
|
|
func impl(_ path: AbsolutePath) -> FileBuildSettings? {
|
|
for package in modulesGraph.packages where path == package.manifest.path {
|
|
let compilerArgs = workspace.interpreterFlags(for: package.path) + [path.pathString]
|
|
return FileBuildSettings(compilerArguments: compilerArgs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if let result = impl(path) {
|
|
return result
|
|
}
|
|
|
|
let canonicalPath = try resolveSymlinks(path)
|
|
return canonicalPath == path ? nil : impl(canonicalPath)
|
|
}
|
|
|
|
/// Retrieve settings for a given header file.
|
|
///
|
|
/// This finds the target the header belongs to based on its location in the file system, retrieves the build settings
|
|
/// for any file within that target and generates compiler arguments by replacing that picked file with the header
|
|
/// file.
|
|
/// This is safe because all files within one target have the same build settings except for reference to the file
|
|
/// itself, which we are replacing.
|
|
private func settings(forHeader path: AbsolutePath, _ language: Language) throws -> FileBuildSettings? {
|
|
func impl(_ path: AbsolutePath) throws -> FileBuildSettings? {
|
|
var dir = path.parentDirectory
|
|
while !dir.isRoot {
|
|
if let buildTarget = sourceDirToTarget[dir] {
|
|
if let sourceFile = buildTarget.sources.first {
|
|
return FileBuildSettings(
|
|
compilerArguments: try buildTarget.compileArguments(for: sourceFile),
|
|
workingDirectory: workspacePath.pathString
|
|
).patching(newFile: path.pathString, originalFile: sourceFile.absoluteString)
|
|
}
|
|
return nil
|
|
}
|
|
dir = dir.parentDirectory
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if let result = try impl(path) {
|
|
return result
|
|
}
|
|
|
|
let canonicalPath = try resolveSymlinks(path)
|
|
return try canonicalPath == path ? nil : impl(canonicalPath)
|
|
}
|
|
}
|
|
|
|
/// Find a Swift Package root directory that contains the given path, if any.
|
|
private func findPackageDirectory(
|
|
containing path: TSCAbsolutePath,
|
|
_ fileSystem: FileSystem
|
|
) -> TSCAbsolutePath? {
|
|
var path = path
|
|
while true {
|
|
let packagePath = path.appending(component: "Package.swift")
|
|
if fileSystem.isFile(packagePath) {
|
|
let contents = try? fileSystem.readFileContents(packagePath)
|
|
if contents?.cString.contains("PackageDescription") == true {
|
|
return path
|
|
}
|
|
}
|
|
|
|
if path.isRoot {
|
|
return nil
|
|
}
|
|
path = path.parentDirectory
|
|
}
|
|
return path
|
|
}
|
|
|
|
extension Basics.Diagnostic.Severity {
|
|
var asLogLevel: LogLevel {
|
|
switch self {
|
|
case .error, .warning: return .default
|
|
case .info: return .info
|
|
case .debug: return .debug
|
|
}
|
|
}
|
|
}
|