Files
sourcekit-lsp/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift
2020-02-21 07:12:47 +01:00

465 lines
14 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
//
//===----------------------------------------------------------------------===//
#if canImport(SPMBuildCore)
import SPMBuildCore
#endif
import Build
import BuildServerProtocol
import LanguageServerProtocol
import LSPLogging
import PackageGraph
import PackageLoading
import PackageModel
import SKCore
import SKSupport
import Workspace
import struct Foundation.URL
/// 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 final class SwiftPMWorkspace {
public enum Error: Swift.Error {
/// Could not find a manifest (Package.swift file). This is not a package.
case noManifest(workspacePath: AbsolutePath)
/// 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: BuildSystemDelegate? = nil
let workspacePath: AbsolutePath
let packageRoot: AbsolutePath
var packageGraph: PackageGraph
let workspace: Workspace
public let buildParameters: BuildParameters
let fileSystem: FileSystem
var fileToTarget: [AbsolutePath: TargetBuildDescription] = [:]
var sourceDirToTarget: [AbsolutePath: TargetBuildDescription] = [:]
/// 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.
/// - Throws: If there is an error loading the package, or no manifest is found.
public init(
workspacePath: AbsolutePath,
toolchainRegistry: ToolchainRegistry,
fileSystem: FileSystem = localFileSystem,
buildSetup: BuildSetup) throws
{
self.workspacePath = workspacePath
self.fileSystem = fileSystem
guard let packageRoot = findPackageDirectory(containing: workspacePath, fileSystem) else {
throw Error.noManifest(workspacePath: workspacePath)
}
self.packageRoot = resolveSymlinks(packageRoot)
guard let destinationToolchainBinDir = toolchainRegistry.default?.swiftc?.parentDirectory else {
throw Error.cannotDetermineHostToolchain
}
let destination = try Destination.hostDestination(destinationToolchainBinDir)
let toolchain = try UserToolchain(destination: destination)
let buildPath: AbsolutePath = buildSetup.path ?? packageRoot.appending(component: ".build")
self.workspace = Workspace(
dataPath: buildPath,
editablesPath: packageRoot.appending(component: "Packages"),
pinsFile: packageRoot.appending(component: "Package.resolved"),
manifestLoader: ManifestLoader(manifestResources: toolchain.manifestResources, cacheDir: buildPath),
delegate: BuildSettingProviderWorkspaceDelegate(),
fileSystem: fileSystem,
skipUpdate: true)
let triple = toolchain.triple
let swiftPMConfiguration: PackageModel.BuildConfiguration
switch buildSetup.configuration {
case .debug:
swiftPMConfiguration = .debug
case .release:
swiftPMConfiguration = .release
}
self.buildParameters = BuildParameters(
dataPath: buildPath.appending(component: triple.tripleString),
configuration: swiftPMConfiguration,
toolchain: toolchain,
flags: buildSetup.flags)
self.packageGraph = PackageGraph(rootPackages: [], requiredDependencies: [])
try reloadPackage()
}
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
///
/// - Returns: nil if `workspacePath` is not part of a package or there is an error.
public convenience init?(url: URL,
toolchainRegistry: ToolchainRegistry,
buildSetup: BuildSetup)
{
do {
try self.init(
workspacePath: try AbsolutePath(validating: url.path),
toolchainRegistry: toolchainRegistry,
fileSystem: localFileSystem,
buildSetup: buildSetup)
} catch Error.noManifest(let path) {
log("could not find manifest, or not a SwiftPM package: \(path)", level: .warning)
return nil
} catch {
log("failed to create \(SwiftPMWorkspace.self): \(error)", level: .error)
return nil
}
}
}
extension SwiftPMWorkspace {
/// (Re-)load the package settings by parsing the manifest and resolving all the targets and
/// dependencies.
func reloadPackage() throws {
let diags = DiagnosticsEngine(handlers: [{ diag in
log(diag.localizedDescription, level: diag.behavior.asLogLevel)
}])
self.packageGraph = self.workspace.loadPackageGraph(
root: PackageGraphRootInput(packages: [packageRoot]),
diagnostics: diags)
let plan = try BuildPlan(
buildParameters: buildParameters,
graph: packageGraph,
diagnostics: diags,
fileSystem: fileSystem)
self.fileToTarget = [AbsolutePath: TargetBuildDescription](
packageGraph.allTargets.flatMap { target in
return target.sources.paths.compactMap {
guard let td = plan.targetMap[target] else {
return nil
}
return (key: $0, value: td)
}
}, uniquingKeysWith: { td, _ in
// FIXME: is there a preferred target?
return td
})
self.sourceDirToTarget = [AbsolutePath: TargetBuildDescription](
packageGraph.allTargets.compactMap { target in
guard let td = plan.targetMap[target] else {
return nil
}
return (key: target.sources.root, value: td)
}, uniquingKeysWith: { td, _ in
// FIXME: is there a preferred target?
return td
})
}
}
extension SwiftPMWorkspace: SKCore.BuildSystem {
public var buildPath: AbsolutePath {
return buildParameters.buildPath
}
public var indexStorePath: AbsolutePath? {
return buildParameters.indexStoreMode == .off ? nil : buildParameters.indexStore
}
public var indexDatabasePath: AbsolutePath? {
return buildPath.appending(components: "index", "db")
}
public func settings(
for uri: DocumentURI,
_ language: Language) -> 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 td = targetDescription(for: path) {
return settings(for: path, language, td)
}
if path.basename == "Package.swift" {
return settings(forPackageManifest: path)
}
if path.extension == "h" {
return settings(forHeader: path, language)
}
return nil
}
/// Register the given file for build-system level change notifications, such as command
/// line flag changes, dependency changes, etc.
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) {
// TODO: Support for change detection (via file watching)
}
/// 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) {
// TODO: Support for change detection (via file watching)
}
public func buildTargets(reply: @escaping (LSPResult<[BuildTarget]>) -> Void) {
// TODO: Support for build targets
reply(.failure(buildTargetsNotSupported))
}
public func buildTargetSources(targets: [BuildTargetIdentifier], reply: @escaping (LSPResult<[SourcesItem]>) -> Void) {
reply(.failure(buildTargetsNotSupported))
}
public func buildTargetOutputPaths(targets: [BuildTargetIdentifier], reply: @escaping (LSPResult<[OutputsItem]>) -> Void) {
reply(.failure(buildTargetsNotSupported))
}
/// Returns the resolved target description for the given file, if one is known.
func targetDescription(for file: AbsolutePath) -> TargetBuildDescription? {
if let td = fileToTarget[file] {
return td
}
let realpath = resolveSymlinks(file)
if realpath != file, let td = fileToTarget[realpath] {
fileToTarget[file] = td
return td
}
return nil
}
}
extension SwiftPMWorkspace {
// MARK: Implementation details
/// Retrieve settings for the given file, which is part of a known target build description.
public func settings(
for path: AbsolutePath,
_ language: Language,
_ td: TargetBuildDescription) -> FileBuildSettings?
{
switch (td, language) {
case (.swift(let td), .swift):
return settings(forSwiftFile: path, td)
case (.clang, .swift):
return nil
case (.clang(let td), _):
return settings(forClangFile: path, language, td)
default:
return nil
}
}
/// Retrieve settings for a package manifest (Package.swift).
func settings(forPackageManifest path: AbsolutePath) -> FileBuildSettings? {
func impl(_ path: AbsolutePath) -> FileBuildSettings? {
for package in packageGraph.packages where path == package.manifest.path {
let compilerArgs = workspace.interpreterFlags(for: package.path) + [path.pathString]
return FileBuildSettings(compilerArguments: compilerArgs, language: .swift)
}
return nil
}
if let result = impl(path) {
return result
}
let canonicalPath = resolveSymlinks(path)
return canonicalPath == path ? nil : impl(canonicalPath)
}
/// Retrieve settings for a given header file.
public func settings(forHeader path: AbsolutePath, _ language: Language) -> FileBuildSettings? {
func impl(_ path: AbsolutePath) -> FileBuildSettings? {
var dir = path.parentDirectory
while !dir.isRoot {
if let td = sourceDirToTarget[dir] {
return settings(for: path, language, td)
}
dir = dir.parentDirectory
}
return nil
}
if let result = impl(path) {
return result
}
let canonicalPath = resolveSymlinks(path)
return canonicalPath == path ? nil : impl(canonicalPath)
}
/// Retrieve settings for the given swift file, which is part of a known target build description.
public func settings(
forSwiftFile path: AbsolutePath,
_ td: SwiftTargetBuildDescription) -> FileBuildSettings?
{
// FIXME: this is re-implementing llbuild's constructCommandLineArgs.
var args: [String] = [
"-module-name",
td.target.c99name,
"-incremental",
"-emit-dependencies",
"-emit-module",
"-emit-module-path",
buildPath.appending(component: "\(td.target.c99name).swiftmodule").pathString
// -output-file-map <path>
]
if td.target.type == .library || td.target.type == .test {
args += ["-parse-as-library"]
}
args += ["-c"]
args += td.target.sources.paths.map { $0.pathString }
args += ["-I", buildPath.pathString]
args += td.compileArguments()
return FileBuildSettings(
compilerArguments: args,
workingDirectory: workspacePath.pathString,
language: .swift)
}
/// Retrieve settings for the given C-family language file, which is part of a known target build
/// description.
///
/// - Note: language must be a C-family language.
public func settings(
forClangFile path: AbsolutePath,
_ language: Language,
_ td: ClangTargetBuildDescription) -> FileBuildSettings?
{
// FIXME: this is re-implementing things from swiftpm's createClangCompileTarget
var args = td.basicArguments()
let compilePath = td.compilePaths().first(where: { $0.source == path })
if let compilePath = compilePath {
args += [
"-MD",
"-MT",
"dependencies",
"-MF",
compilePath.deps.pathString,
]
}
switch language {
case .c:
if let std = td.clangTarget.cLanguageStandard {
args += ["-std=\(std)"]
}
case .cpp:
if let std = td.clangTarget.cxxLanguageStandard {
args += ["-std=\(std)"]
}
default:
break
}
if let compilePath = compilePath {
args += [
"-c",
compilePath.source.pathString,
"-o",
compilePath.object.pathString
]
} else if path.extension == "h" {
args += ["-c"]
if let xflag = language.xflagHeader {
args += ["-x", xflag]
}
args += [path.pathString]
} else {
args += [
"-c",
path.pathString,
]
}
return FileBuildSettings(
compilerArguments: args,
workingDirectory: workspacePath.pathString,
language: language)
}
}
/// Find a Swift Package root directory that contains the given path, if any.
private func findPackageDirectory(
containing path: AbsolutePath,
_ fileSystem: FileSystem) -> AbsolutePath? {
var path = path
while !fileSystem.isFile(path.appending(component: "Package.swift")) {
if path.isRoot {
return nil
}
path = path.parentDirectory
}
return path
}
public final class BuildSettingProviderWorkspaceDelegate: WorkspaceDelegate {
public func packageGraphWillLoad(
currentGraph: PackageGraph,
dependencies: AnySequence<ManagedDependency>,
missingURLs: Set<String>)
{}
public func fetchingWillBegin(repository: String) {}
public func fetchingDidFinish(repository: String, diagnostic: TSCBasic.Diagnostic?) {}
public func cloning(repository: String) {}
public func removing(repository: String) {}
public func managedDependenciesDidUpdate(_ dependencies: AnySequence<ManagedDependency>) {}
}
extension TSCBasic.Diagnostic.Behavior {
var asLogLevel: LogLevel {
switch self {
case .error: return .error
case .warning: return .warning
default: return .info
}
}
}