Skip to content

Public API for getting information about build targets #6763

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 1 commit into from
Dec 8, 2023
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
18 changes: 18 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ let swiftPMDataModelProduct = (
"PackageMetadata",
"PackageModel",
"SourceControl",
"SourceKitLSPAPI",
"Workspace",
]
)
Expand Down Expand Up @@ -148,6 +149,14 @@ let package = Package(
linkerSettings: packageLibraryLinkSettings
),

.target(
name: "SourceKitLSPAPI",
dependencies: [
"Build",
"SPMBuildCore"
]
),

// MARK: SwiftPM specific support libraries

.systemLibrary(name: "SPMSQLite3", pkgConfig: systemSQLitePkgConfig),
Expand Down Expand Up @@ -518,6 +527,7 @@ let package = Package(
name: "SPMTestSupport",
dependencies: [
"Basics",
"Build",
"PackageFingerprint",
"PackageGraph",
"PackageLoading",
Expand All @@ -537,6 +547,14 @@ let package = Package(

// MARK: SwiftPM tests

.testTarget(
name: "SourceKitLSPAPITests",
dependencies: [
"SourceKitLSPAPI",
"SPMTestSupport",
]
),

.testTarget(
name: "BasicsTests",
dependencies: ["Basics", "SPMTestSupport", "tsan_utils"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,42 @@ public final class ClangTargetBuildDescription {
return args
}

public func emitCommandLine(for filePath: AbsolutePath) throws -> [String] {
let standards = [
(clangTarget.cxxLanguageStandard, SupportedLanguageExtension.cppExtensions),
(clangTarget.cLanguageStandard, SupportedLanguageExtension.cExtensions),
]

guard let path = try self.compilePaths().first(where: { $0.source == filePath }) else {
throw BuildDescriptionError.requestedFileNotPartOfTarget(
targetName: self.target.name,
requestedFilePath: filePath
)
}

let isCXX = path.source.extension.map { SupportedLanguageExtension.cppExtensions.contains($0) } ?? false
let isC = path.source.extension.map { $0 == SupportedLanguageExtension.c.rawValue } ?? false

var args = try basicArguments(isCXX: isCXX, isC: isC)

args += ["-MD", "-MT", "dependencies", "-MF", path.deps.pathString]

// Add language standard flag if needed.
if let ext = path.source.extension {
for (standard, validExtensions) in standards {
if let standard, validExtensions.contains(ext) {
args += ["-std=\(standard)"]
}
}
}

args += ["-c", path.source.pathString, "-o", path.object.pathString]

let clangCompiler = try buildParameters.toolchain.getClangCompiler().pathString
args.insert(clangCompiler, at: 0)
return args
}

/// Returns the build flags from the declared build settings.
private func buildSettingsFlags() throws -> [String] {
let scope = buildParameters.createScope(for: target)
Expand Down
4 changes: 4 additions & 0 deletions Sources/Build/BuildDescription/TargetBuildDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import class PackageGraph.ResolvedTarget
import struct PackageModel.Resource
import struct SPMBuildCore.BuildToolPluginInvocationResult

public enum BuildDescriptionError: Swift.Error {
case requestedFileNotPartOfTarget(targetName: String, requestedFilePath: AbsolutePath)
}

/// A target description which can either be for a Swift or Clang target.
public enum TargetBuildDescription {
/// Swift target description.
Expand Down
26 changes: 1 addition & 25 deletions Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ extension LLBuildManifestBuilder {
func createClangCompileCommand(
_ target: ClangTargetBuildDescription
) throws {
let standards = [
(target.clangTarget.cxxLanguageStandard, SupportedLanguageExtension.cppExtensions),
(target.clangTarget.cLanguageStandard, SupportedLanguageExtension.cExtensions),
]

var inputs: [Node] = []

// Add resources node as the input to the target. This isn't great because we
Expand Down Expand Up @@ -79,26 +74,7 @@ extension LLBuildManifestBuilder {
var objectFileNodes: [Node] = []

for path in try target.compilePaths() {
let isCXX = path.source.extension.map { SupportedLanguageExtension.cppExtensions.contains($0) } ?? false
let isC = path.source.extension.map { $0 == SupportedLanguageExtension.c.rawValue } ?? false

var args = try target.basicArguments(isCXX: isCXX, isC: isC)

args += ["-MD", "-MT", "dependencies", "-MF", path.deps.pathString]

// Add language standard flag if needed.
if let ext = path.source.extension {
for (standard, validExtensions) in standards {
if let standard, validExtensions.contains(ext) {
args += ["-std=\(standard)"]
}
}
}

args += ["-c", path.source.pathString, "-o", path.object.pathString]

let clangCompiler = try target.buildParameters.toolchain.getClangCompiler().pathString
args.insert(clangCompiler, at: 0)
let args = try target.emitCommandLine(for: path.source)

let objectFileNode: Node = .file(path.object)
objectFileNodes.append(objectFileNode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,63 +10,66 @@
//
//===----------------------------------------------------------------------===//

@testable import PackageModel
@testable import TSCUtility
@testable import Build
import Basics
import Build
import PackageModel
import SPMBuildCore
import TSCUtility
import XCTest

struct MockToolchain: PackageModel.Toolchain {
public struct MockToolchain: PackageModel.Toolchain {
#if os(Windows)
let librarianPath = AbsolutePath("/fake/path/to/link.exe")
public let librarianPath = AbsolutePath("/fake/path/to/link.exe")
#elseif os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
let librarianPath = AbsolutePath("/fake/path/to/libtool")
public let librarianPath = AbsolutePath("/fake/path/to/libtool")
#else
let librarianPath = AbsolutePath("/fake/path/to/llvm-ar")
public let librarianPath = AbsolutePath("/fake/path/to/llvm-ar")
#endif
let swiftCompilerPath = AbsolutePath("/fake/path/to/swiftc")
let includeSearchPaths = [AbsolutePath]()
let librarySearchPaths = [AbsolutePath]()
let swiftResourcesPath: AbsolutePath? = nil
let swiftStaticResourcesPath: AbsolutePath? = nil
let isSwiftDevelopmentToolchain = false
let sdkRootPath: AbsolutePath? = nil
let swiftPluginServerPath: AbsolutePath? = nil
let extraFlags = PackageModel.BuildFlags()
let installedSwiftPMConfiguration = InstalledSwiftPMConfiguration.default

func getClangCompiler() throws -> AbsolutePath {
public let swiftCompilerPath = AbsolutePath("/fake/path/to/swiftc")
public let includeSearchPaths = [AbsolutePath]()
public let librarySearchPaths = [AbsolutePath]()
public let swiftResourcesPath: AbsolutePath? = nil
public let swiftStaticResourcesPath: AbsolutePath? = nil
public let isSwiftDevelopmentToolchain = false
public let sdkRootPath: AbsolutePath? = nil
public let swiftPluginServerPath: AbsolutePath? = nil
public let extraFlags = PackageModel.BuildFlags()
public let installedSwiftPMConfiguration = InstalledSwiftPMConfiguration.default

public func getClangCompiler() throws -> AbsolutePath {
return "/fake/path/to/clang"
}

func _isClangCompilerVendorApple() throws -> Bool? {
public func _isClangCompilerVendorApple() throws -> Bool? {
#if os(macOS)
return true
#else
return false
#endif
}

public init() {
}
}


extension Basics.Triple {
static let x86_64MacOS = try! Self("x86_64-apple-macosx")
static let x86_64Linux = try! Self("x86_64-unknown-linux-gnu")
static let arm64Linux = try! Self("aarch64-unknown-linux-gnu")
static let arm64Android = try! Self("aarch64-unknown-linux-android")
static let windows = try! Self("x86_64-unknown-windows-msvc")
static let wasi = try! Self("wasm32-unknown-wasi")
public static let x86_64MacOS = try! Self("x86_64-apple-macosx")
public static let x86_64Linux = try! Self("x86_64-unknown-linux-gnu")
public static let arm64Linux = try! Self("aarch64-unknown-linux-gnu")
public static let arm64Android = try! Self("aarch64-unknown-linux-android")
public static let windows = try! Self("x86_64-unknown-windows-msvc")
public static let wasi = try! Self("wasm32-unknown-wasi")
}

let hostTriple = try! UserToolchain.default.targetTriple
public let hostTriple = try! UserToolchain.default.targetTriple
#if os(macOS)
let defaultTargetTriple: String = hostTriple.tripleString(forPlatformVersion: "10.13")
public let defaultTargetTriple: String = hostTriple.tripleString(forPlatformVersion: "10.13")
#else
let defaultTargetTriple: String = hostTriple.tripleString
public let defaultTargetTriple: String = hostTriple.tripleString
#endif

func mockBuildParameters(
public func mockBuildParameters(
buildPath: AbsolutePath = "/path/to/build",
config: BuildConfiguration = .debug,
toolchain: PackageModel.Toolchain = MockToolchain(),
Expand Down Expand Up @@ -108,7 +111,7 @@ func mockBuildParameters(
)
}

func mockBuildParameters(environment: BuildEnvironment) -> BuildParameters {
public func mockBuildParameters(environment: BuildEnvironment) -> BuildParameters {
let triple: Basics.Triple
switch environment.platform {
case .macOS:
Expand All @@ -130,34 +133,34 @@ enum BuildError: Swift.Error {
case error(String)
}

struct BuildPlanResult {
public struct BuildPlanResult {

let plan: Build.BuildPlan
let targetMap: [String: TargetBuildDescription]
let productMap: [String: Build.ProductBuildDescription]
public let plan: Build.BuildPlan
public let targetMap: [String: TargetBuildDescription]
public let productMap: [String: Build.ProductBuildDescription]

init(plan: Build.BuildPlan) throws {
public init(plan: Build.BuildPlan) throws {
self.plan = plan
self.productMap = try Dictionary(throwingUniqueKeysWithValues: plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.map{ ($0.product.name, $0) })
self.targetMap = try Dictionary(throwingUniqueKeysWithValues: plan.targetMap.map{ ($0.0.name, $0.1) })
}

func checkTargetsCount(_ count: Int, file: StaticString = #file, line: UInt = #line) {
public func checkTargetsCount(_ count: Int, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(plan.targetMap.count, count, file: file, line: line)
}

func checkProductsCount(_ count: Int, file: StaticString = #file, line: UInt = #line) {
public func checkProductsCount(_ count: Int, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(plan.productMap.count, count, file: file, line: line)
}

func target(for name: String) throws -> TargetBuildDescription {
public func target(for name: String) throws -> TargetBuildDescription {
guard let target = targetMap[name] else {
throw BuildError.error("Target \(name) not found.")
}
return target
}

func buildProduct(for name: String) throws -> Build.ProductBuildDescription {
public func buildProduct(for name: String) throws -> Build.ProductBuildDescription {
guard let product = productMap[name] else {
// <rdar://problem/30162871> Display the thrown error on macOS
throw BuildError.error("Product \(name) not found.")
Expand All @@ -167,7 +170,7 @@ struct BuildPlanResult {
}

extension TargetBuildDescription {
func swiftTarget() throws -> SwiftTargetBuildDescription {
public func swiftTarget() throws -> SwiftTargetBuildDescription {
switch self {
case .swift(let target):
return target
Expand All @@ -176,7 +179,7 @@ extension TargetBuildDescription {
}
}

func clangTarget() throws -> ClangTargetBuildDescription {
public func clangTarget() throws -> ClangTargetBuildDescription {
switch self {
case .clang(let target):
return target
Expand Down
81 changes: 81 additions & 0 deletions Sources/SourceKitLSPAPI/BuildDescription.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation

/*private*/ import struct Basics.AbsolutePath
/*private*/ import func Basics.resolveSymlinks
// FIXME: should not import this module
import Build
// FIXME: should be internal imports
import PackageGraph
/*private*/ import SPMBuildCore

public protocol BuildTarget {
var sources: [URL] { get }

func compileArguments(for fileURL: URL) throws -> [String]
}

extension ClangTargetBuildDescription: BuildTarget {
public var sources: [URL] {
return (try? compilePaths().map { URL(fileURLWithPath: $0.source.pathString) }) ?? []
}

public func compileArguments(for fileURL: URL) throws -> [String] {
let filePath = try resolveSymlinks(try AbsolutePath(validating: fileURL.path))
return try self.emitCommandLine(for: filePath)
}
}

private struct WrappedSwiftTargetBuildDescription: BuildTarget {
private let description: SwiftTargetBuildDescription

init(description: SwiftTargetBuildDescription) {
self.description = description
}

var sources: [URL] {
return description.sources.map { URL(fileURLWithPath: $0.pathString) }
}

func compileArguments(for fileURL: URL) throws -> [String] {
// Note: we ignore the `fileURL` here as the expectation is that we get a commandline for the entire target in case of Swift.
return try description.emitCommandLine(scanInvocation: false)
}
}

public struct BuildDescription {
private let buildPlan: Build.BuildPlan

// FIXME: should not use `BuildPlan` in the public interface
public init(buildPlan: Build.BuildPlan) {
self.buildPlan = buildPlan
}

// FIXME: should not use `ResolvedTarget` in the public interface
public func getBuildTarget(for target: ResolvedTarget) -> BuildTarget? {
if let description = buildPlan.targetMap[target] {
switch description {
case .clang(let description):
return description
case .swift(let description):
return WrappedSwiftTargetBuildDescription(description: description)
}
} else {
if target.type == .plugin, let package = self.buildPlan.graph.package(for: target) {
return PluginTargetBuildDescription(target: target, toolsVersion: package.manifest.toolsVersion)
}
return nil
}
}
}
Loading