diff --git a/Sources/Basics/Collections/IdentifiableSet.swift b/Sources/Basics/Collections/IdentifiableSet.swift index 46383085b2b..1941263c1d2 100644 --- a/Sources/Basics/Collections/IdentifiableSet.swift +++ b/Sources/Basics/Collections/IdentifiableSet.swift @@ -42,6 +42,10 @@ public struct IdentifiableSet: Collection { Index(storageIndex: self.storage.elements.endIndex) } + public var values: some Sequence { + self.storage.values + } + public subscript(position: Index) -> Element { self.storage.elements[position.storageIndex].value } diff --git a/Sources/Build/BuildDescription/ClangModuleBuildDescription.swift b/Sources/Build/BuildDescription/ClangModuleBuildDescription.swift index 08416d19730..b149675ebe2 100644 --- a/Sources/Build/BuildDescription/ClangModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/ClangModuleBuildDescription.swift @@ -42,6 +42,11 @@ public final class ClangModuleBuildDescription { /// The build parameters. let buildParameters: BuildParameters + /// The destination for while this module is built. + public var destination: BuildParameters.Destination { + self.buildParameters.destination + } + /// The build environment. var buildEnvironment: BuildEnvironment { buildParameters.buildEnvironment @@ -521,3 +526,17 @@ public final class ClangModuleBuildDescription { ) } } + +extension ClangModuleBuildDescription { + package func dependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.clang(self).dependencies(using: plan) + } + + package func recursiveDependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.clang(self).recursiveDependencies(using: plan) + } +} \ No newline at end of file diff --git a/Sources/Build/BuildDescription/ModuleBuildDescription.swift b/Sources/Build/BuildDescription/ModuleBuildDescription.swift index b2e130cbedc..8f52d45f370 100644 --- a/Sources/Build/BuildDescription/ModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/ModuleBuildDescription.swift @@ -13,6 +13,7 @@ import Basics import struct PackageGraph.ResolvedModule import struct PackageGraph.ResolvedPackage +import struct PackageGraph.ResolvedProduct import struct PackageModel.Resource import struct PackageModel.ToolsVersion import struct SPMBuildCore.BuildToolPluginInvocationResult @@ -121,6 +122,15 @@ public enum ModuleBuildDescription: SPMBuildCore.ModuleBuildDescription { } } + var destination: BuildParameters.Destination { + switch self { + case .swift(let buildDescription): + buildDescription.destination + case .clang(let buildDescription): + buildDescription.destination + } + } + var toolsVersion: ToolsVersion { switch self { case .swift(let buildDescription): @@ -139,3 +149,47 @@ public enum ModuleBuildDescription: SPMBuildCore.ModuleBuildDescription { } } } + +extension ModuleBuildDescription: Identifiable { + public struct ID: Hashable { + let moduleID: ResolvedModule.ID + let destination: BuildParameters.Destination + } + + public var id: ID { + ID(moduleID: self.module.id, destination: self.destination) + } +} + +extension ModuleBuildDescription { + package enum Dependency { + /// Not all of the modules and products have build descriptions + case product(ResolvedProduct, ProductBuildDescription?) + case module(ResolvedModule, ModuleBuildDescription?) + } + + package func dependencies(using plan: BuildPlan) -> [Dependency] { + self.module + .dependencies(satisfying: self.buildParameters.buildEnvironment) + .map { + switch $0 { + case .product(let product, _): + let productDescription = plan.description(for: product, context: self.destination) + return .product(product, productDescription) + case .module(let module, _): + let moduleDescription = plan.description(for: module, context: self.destination) + return .module(module, moduleDescription) + } + } + } + + package func recursiveDependencies(using plan: BuildPlan) -> [Dependency] { + var dependencies: [Dependency] = [] + plan.traverseDependencies(of: self) { product, _, description in + dependencies.append(.product(product, description)) + } onModule: { module, _, description in + dependencies.append(.module(module, description)) + } + return dependencies + } +} diff --git a/Sources/Build/BuildDescription/ProductBuildDescription.swift b/Sources/Build/BuildDescription/ProductBuildDescription.swift index f04fb7ec09c..6a3ec17c26c 100644 --- a/Sources/Build/BuildDescription/ProductBuildDescription.swift +++ b/Sources/Build/BuildDescription/ProductBuildDescription.swift @@ -37,6 +37,11 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription /// The build parameters. public let buildParameters: BuildParameters + /// The destination for while this product is built. + public var destination: BuildParameters.Destination { + self.buildParameters.destination + } + /// All object files to link into this product. /// // Computed during build planning. @@ -397,6 +402,17 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription } } +extension ProductBuildDescription: Identifiable { + public struct ID: Hashable { + let productID: ResolvedProduct.ID + let destination: BuildParameters.Destination + } + + public var id: ID { + ID(productID: self.product.id, destination: self.destination) + } +} + extension SortedArray where Element == AbsolutePath { public static func +=(lhs: inout SortedArray, rhs: S) where S.Iterator.Element == AbsolutePath { lhs.insert(contentsOf: rhs) diff --git a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift index 693ac4da8e6..63cbfe2ab61 100644 --- a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift @@ -49,6 +49,11 @@ public final class SwiftModuleBuildDescription { /// The build parameters for this target. let buildParameters: BuildParameters + /// The destination for while this module is built. + public var destination: BuildParameters.Destination { + self.buildParameters.destination + } + /// The build parameters for the macro dependencies of this target. let macroBuildParameters: BuildParameters @@ -983,3 +988,17 @@ public final class SwiftModuleBuildDescription { return arguments } } + +extension SwiftModuleBuildDescription { + package func dependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.swift(self).dependencies(using: plan) + } + + package func recursiveDependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.swift(self).recursiveDependencies(using: plan) + } +} \ No newline at end of file diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift index a059bcce1eb..369206bddb1 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift @@ -32,30 +32,36 @@ extension LLBuildManifestBuilder { inputs.append(resourcesNode) } - func addStaticTargetInputs(_ target: ResolvedModule) { - if case .swift(let desc)? = self.plan.targetMap[target.id], target.type == .library { + func addStaticTargetInputs(_ description: ModuleBuildDescription?) { + if case .swift(let desc) = description, desc.target.type == .library { inputs.append(file: desc.moduleOutputPath) } } - for dependency in target.target.dependencies(satisfying: target.buildEnvironment) { + for dependency in target.dependencies(using: self.plan) { switch dependency { - case .module(let target, _): - addStaticTargetInputs(target) + case .module(_, let description): + addStaticTargetInputs(description) - case .product(let product, _): + case .product(let product, let productDescription): switch product.type { case .executable, .snippet, .library(.dynamic), .macro: - guard let planProduct = plan.productMap[product.id] else { - throw InternalError("unknown product \(product)") + guard let productDescription else { + throw InternalError("No build description for product: \(product)") } // Establish a dependency on binary of the product. - let binary = try planProduct.binaryPath - inputs.append(file: binary) + try inputs.append(file: productDescription.binaryPath) case .library(.automatic), .library(.static), .plugin: - for target in product.modules { - addStaticTargetInputs(target) + for module in product.modules { + guard let dependencyDescription = self.plan.description( + for: module, + context: product.type == .plugin ? .host : target.destination + ) else + { + throw InternalError("unknown module: \(module)") + } + addStaticTargetInputs(dependencyDescription) } case .test: break diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift index 822ecc66a07..0a7da8c47b1 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift @@ -191,10 +191,8 @@ extension LLBuildManifestBuilder { public func addTargetsToExplicitBuildManifest() throws { // Sort the product targets in topological order in order to collect and "bubble up" // their respective dependency graphs to the depending targets. - let nodes = self.plan.targets.compactMap { - ResolvedModule.Dependency.module($0.module, conditions: []) - } - let allPackageDependencies = try topologicalSort(nodes, successors: { $0.dependencies }) + let allPackageDependencies = self.plan.targets.flatMap { $0.recursiveDependencies(using: self.plan) } + // Instantiate the inter-module dependency oracle which will cache commonly-scanned // modules across targets' Driver instances. let dependencyOracle = InterModuleDependencyOracle() @@ -206,14 +204,15 @@ extension LLBuildManifestBuilder { // Create commands for all module descriptions in the plan. for dependency in allPackageDependencies.reversed() { - guard case .module(let target, _) = dependency else { + guard case .module(let module, let description) = dependency else { // Product dependency build jobs are added after the fact. // Targets that depend on product dependencies will expand the corresponding // product into its constituent targets. continue } - guard target.underlying.type != .systemModule, - target.underlying.type != .binary + + guard module.underlying.type != .systemModule, + module.underlying.type != .binary else { // Much like non-Swift targets, system modules will consist of a modulemap // somewhere in the filesystem, with the path to that module being either @@ -225,9 +224,11 @@ extension LLBuildManifestBuilder { // be able to detect such targets' modules. continue } - guard let description = plan.targetMap[target.id] else { - throw InternalError("Expected description for target \(target)") + + guard let description else { + throw InternalError("Expected description for module \(module)") } + switch description { case .swift(let desc): try self.createExplicitSwiftTargetCompileCommand( @@ -319,32 +320,29 @@ extension LLBuildManifestBuilder { for targetDescription: ModuleBuildDescription, dependencyModuleDetailsMap: inout SwiftDriver.ExternalTargetModuleDetailsMap ) throws { - for dependency in targetDescription.module.dependencies(satisfying: targetDescription.buildParameters.buildEnvironment) { + for dependency in targetDescription.dependencies(using: self.plan) { switch dependency { - case .product: - // Product dependencies are broken down into the targets that make them up. - guard let dependencyProduct = dependency.product else { - throw InternalError("unknown dependency product for \(dependency)") - } - for dependencyProductTarget in dependencyProduct.modules { - guard let dependencyTargetDescription = self.plan.targetMap[dependencyProductTarget.id] else { - throw InternalError("unknown dependency target for \(dependencyProductTarget)") + case .product(let product, let productDescription): + for productDependency in product.modules { + guard let dependencyModuleDescription = self.plan.description( + for: productDependency, + context: productDescription?.destination ?? targetDescription.destination + ) else + { + throw InternalError("unknown dependency target for \(productDependency)") } try self.addTargetDependencyInfo( - for: dependencyTargetDescription, + for: dependencyModuleDescription, dependencyModuleDetailsMap: &dependencyModuleDetailsMap ) } - case .module: - // Product dependencies are broken down into the targets that make them up. - guard - let dependencyTarget = dependency.module, - let dependencyTargetDescription = self.plan.targetMap[dependencyTarget.id] - else { - throw InternalError("unknown dependency target for \(dependency)") + case .module(let dependencyModule, let dependencyDescription): + guard let dependencyDescription else { + throw InternalError("No build description for module: \(dependencyModule)") } + // Product dependencies are broken down into the targets that make them up. try self.addTargetDependencyInfo( - for: dependencyTargetDescription, + for: dependencyDescription, dependencyModuleDetailsMap: &dependencyModuleDetailsMap ) } @@ -422,63 +420,73 @@ extension LLBuildManifestBuilder { let prepareForIndexing = target.buildParameters.prepareForIndexing - func addStaticTargetInputs(_ target: ResolvedModule) throws { + func addStaticTargetInputs(_ module: ResolvedModule, _ description: ModuleBuildDescription?) throws { // Ignore C Modules. - if target.underlying is SystemLibraryModule { return } + if module.underlying is SystemLibraryModule { return } // Ignore Binary Modules. - if target.underlying is BinaryModule { return } + if module.underlying is BinaryModule { return } // Ignore Plugin Modules. - if target.underlying is PluginModule { return } + if module.underlying is PluginModule { return } + + guard let description else { + throw InternalError("No build description for module: \(module)") + } // Depend on the binary for executable targets. - if target.type == .executable && prepareForIndexing == .off { - // FIXME: Optimize. + if module.type == .executable && prepareForIndexing == .off { + // FIXME: Optimize. Build plan could build a mapping between executable modules + // and their products to speed up search here, which is inefficient if the plan + // contains a lot of products. if let productDescription = try plan.productMap.values.first(where: { - try $0.product.type == .executable && $0.product.executableModule.id == target.id + try $0.product.type == .executable && + $0.product.executableModule.id == module.id && + $0.destination == description.destination }) { try inputs.append(file: productDescription.binaryPath) } return } - switch self.plan.targetMap[target.id] { - case .swift(let target)?: - inputs.append(file: target.moduleOutputPath) - case .clang(let target)?: + switch description { + case .swift(let swiftDescription): + inputs.append(file: swiftDescription.moduleOutputPath) + case .clang(let clangDescription): if prepareForIndexing != .off { // In preparation, we're only building swiftmodules // propagate the dependency to the header files in this target - for header in target.clangTarget.headers { + for header in clangDescription.clangTarget.headers { inputs.append(file: header) } } else { - for object in try target.objects { + for object in try clangDescription.objects { inputs.append(file: object) } } - case nil: - throw InternalError("unexpected: target \(target) not in target map \(self.plan.targetMap)") } } - for dependency in target.target.dependencies(satisfying: target.buildParameters.buildEnvironment) { + for dependency in target.dependencies(using: self.plan) { switch dependency { - case .module(let target, _): - try addStaticTargetInputs(target) + case .module(let module, let description): + try addStaticTargetInputs(module, description) - case .product(let product, _): + case .product(let product, let productDescription): switch product.type { case .executable, .snippet, .library(.dynamic), .macro: - guard let planProduct = plan.productMap[product.id] else { - throw InternalError("unknown product \(product)") + guard let productDescription else { + throw InternalError("No description for product: \(product)") } // Establish a dependency on binary of the product. - try inputs.append(file: planProduct.binaryPath) + try inputs.append(file: productDescription.binaryPath) // For automatic and static libraries, and plugins, add their targets as static input. case .library(.automatic), .library(.static), .plugin: - for target in product.modules { - try addStaticTargetInputs(target) + for module in product.modules { + let description = self.plan.description( + for: module, + context: product.type == .plugin ? .host : target.destination + ) + try addStaticTargetInputs(module, description) } case .test: diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder.swift index b12be0a1832..6b74a890067 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder.swift @@ -102,7 +102,7 @@ public class LLBuildManifestBuilder { try addTargetsToExplicitBuildManifest() } else { // Create commands for all target descriptions in the plan. - for (_, description) in self.plan.targetMap { + for description in self.plan.targetMap { switch description { case .swift(let desc): try self.createSwiftCompileCommand(desc) @@ -116,7 +116,7 @@ public class LLBuildManifestBuilder { try self.addTestEntryPointGenerationCommand() // Create command for all products in the plan. - for (_, description) in self.plan.productMap { + for description in self.plan.productMap { try self.createProductCommand(description) } @@ -133,7 +133,7 @@ public class LLBuildManifestBuilder { addPackageStructureCommand() - for (_, description) in self.plan.targetMap { + for description in self.plan.targetMap { switch description { case .swift(let desc): try self.createSwiftCompileCommand(desc) @@ -148,7 +148,7 @@ public class LLBuildManifestBuilder { } } - for (_, description) in self.plan.productMap { + for description in self.plan.productMap { // Need to generate macro products switch description.product.type { case .macro, .plugin: @@ -271,7 +271,7 @@ extension LLBuildManifestBuilder { private func addTestDiscoveryGenerationCommand() throws { for testDiscoveryTarget in self.plan.targets.compactMap(\.testDiscoveryTargetBuildDescription) { let testTargets = testDiscoveryTarget.target.dependencies - .compactMap(\.module).compactMap { self.plan.targetMap[$0.id] } + .compactMap(\.module).compactMap { self.plan.description(for: $0, context: testDiscoveryTarget.destination) } let objectFiles = try testTargets.flatMap { try $0.objects }.sorted().map(Node.file) let outputs = testDiscoveryTarget.target.sources.paths @@ -288,18 +288,22 @@ extension LLBuildManifestBuilder { } private func addTestEntryPointGenerationCommand() throws { - for target in self.plan.targets { - guard case .swift(let target) = target, - case .entryPoint(let isSynthesized) = target.testTargetRole, + for module in self.plan.targets { + guard case .swift(let swiftModule) = module, + case .entryPoint(let isSynthesized) = swiftModule.testTargetRole, isSynthesized else { continue } - let testEntryPointTarget = target + let testEntryPointTarget = swiftModule // Get the Swift target build descriptions of all discovery modules this synthesized entry point target // depends on. - let discoveredTargetDependencyBuildDescriptions = testEntryPointTarget.target.dependencies - .compactMap(\.module?.id) - .compactMap { self.plan.targetMap[$0] } + let discoveredTargetDependencyBuildDescriptions = module.dependencies(using: self.plan) + .compactMap { + if case .module(_, let description) = $0 { + return description + } + return nil + } .compactMap(\.testDiscoveryTargetBuildDescription) // The module outputs of the discovery modules this synthesized entry point target depends on are diff --git a/Sources/Build/BuildPlan/BuildPlan+Clang.swift b/Sources/Build/BuildPlan/BuildPlan+Clang.swift index 1c7c9575a1a..ca83e4b2e6a 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Clang.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Clang.swift @@ -18,12 +18,12 @@ import class PackageModel.SystemLibraryModule extension BuildPlan { /// Plan a Clang target. func plan(clangTarget: ClangModuleBuildDescription) throws { - let dependencies = try clangTarget.target.recursiveDependencies(satisfying: clangTarget.buildEnvironment) + let dependencies = clangTarget.recursiveDependencies(using: self) - for case .module(let dependency, _) in dependencies { + for case .module(let dependency, let description) in dependencies { switch dependency.underlying { case is SwiftModule: - if case let .swift(dependencyTargetDescription)? = targetMap[dependency.id] { + if case let .swift(dependencyTargetDescription)? = description { if let moduleMap = dependencyTargetDescription.moduleMap { clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"] } @@ -34,7 +34,7 @@ extension BuildPlan { clangTarget.additionalFlags += ["-I", target.includeDir.pathString] // Add the modulemap of the dependency if it has one. - if case let .clang(dependencyTargetDescription)? = targetMap[dependency.id] { + if case let .clang(dependencyTargetDescription)? = description { if let moduleMap = dependencyTargetDescription.moduleMap { clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"] } diff --git a/Sources/Build/BuildPlan/BuildPlan+Product.swift b/Sources/Build/BuildPlan/BuildPlan+Product.swift index 1f2a511d6f8..11879b8d214 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Product.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Product.swift @@ -77,12 +77,12 @@ extension BuildPlan { } } - for target in dependencies.staticTargets { - switch target.underlying { + for module in dependencies.staticTargets { + switch module.underlying { case is SwiftModule: // Swift targets are guaranteed to have a corresponding Swift description. - guard case .swift(let description) = self.targetMap[target.id] else { - throw InternalError("unknown target \(target)") + guard case .swift(let description) = self.description(for: module, context: buildProduct.destination) else { + throw InternalError("unknown module \(module)") } // Based on the debugging strategy, we either need to pass swiftmodule paths to the @@ -103,16 +103,16 @@ extension BuildPlan { buildProduct.staticTargets = dependencies.staticTargets buildProduct.dylibs = try dependencies.dylibs.map { - guard let product = self.productMap[$0.id] else { + guard let product = self.description(for: $0, context: buildProduct.destination) else { throw InternalError("unknown product \($0)") } return product } - buildProduct.objects += try dependencies.staticTargets.flatMap { targetName -> [AbsolutePath] in - guard let target = self.targetMap[targetName.id] else { - throw InternalError("unknown target \(targetName)") + buildProduct.objects += try dependencies.staticTargets.flatMap { module -> [AbsolutePath] in + guard let description = self.description(for: module, context: buildProduct.destination) else { + throw InternalError("unknown module \(module)") } - return try target.objects + return try description.objects } buildProduct.libraryBinaryPaths = dependencies.libraryBinaryPaths diff --git a/Sources/Build/BuildPlan/BuildPlan+Swift.swift b/Sources/Build/BuildPlan/BuildPlan+Swift.swift index 9a56e5732c5..7d387f8cfd5 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Swift.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Swift.swift @@ -20,11 +20,10 @@ extension BuildPlan { func plan(swiftTarget: SwiftModuleBuildDescription) throws { // We need to iterate recursive dependencies because Swift compiler needs to see all the targets a target // depends on. - let environment = swiftTarget.buildParameters.buildEnvironment - for case .module(let dependency, _) in try swiftTarget.target.recursiveDependencies(satisfying: environment) { + for case .module(let dependency, let description) in swiftTarget.recursiveDependencies(using: self) { switch dependency.underlying { case let underlyingTarget as ClangModule where underlyingTarget.type == .library: - guard case let .clang(target)? = targetMap[dependency.id] else { + guard case let .clang(target)? = description else { throw InternalError("unexpected clang target \(underlyingTarget)") } // Add the path to modulemap of the dependency. Currently we require that all Clang targets have a diff --git a/Sources/Build/BuildPlan/BuildPlan+Test.swift b/Sources/Build/BuildPlan/BuildPlan+Test.swift index 9b67e656fdd..94a14db29ff 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Test.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Test.swift @@ -34,7 +34,7 @@ import protocol TSCBasic.FileSystem extension BuildPlan { static func makeDerivedTestTargets( - testProducts: [(product: ResolvedProduct, buildDescription: ProductBuildDescription)], + testProducts: [ProductBuildDescription], destinationBuildParameters: BuildParameters, toolsBuildParameters: BuildParameters, shouldDisableSandbox: Bool, @@ -51,7 +51,8 @@ extension BuildPlan { var isDiscoveryEnabledRedundantly = explicitlyEnabledDiscovery && !isEntryPointPathSpecifiedExplicitly var result: [(ResolvedProduct, SwiftModuleBuildDescription?, SwiftModuleBuildDescription)] = [] - for (testProduct, testBuildDescription) in testProducts { + for testBuildDescription in testProducts { + let testProduct = testBuildDescription.product let package = testBuildDescription.package isDiscoveryEnabledRedundantly = isDiscoveryEnabledRedundantly && nil == testProduct.testEntryPointModule diff --git a/Sources/Build/BuildPlan/BuildPlan.swift b/Sources/Build/BuildPlan/BuildPlan.swift index c5648a2419e..31a193e85d0 100644 --- a/Sources/Build/BuildPlan/BuildPlan.swift +++ b/Sources/Build/BuildPlan/BuildPlan.swift @@ -197,10 +197,10 @@ public class BuildPlan: SPMBuildCore.BuildPlan { public let graph: ModulesGraph /// The target build description map. - public let targetMap: [ResolvedModule.ID: ModuleBuildDescription] + public let targetMap: IdentifiableSet /// The product build description map. - public let productMap: [ResolvedProduct.ID: ProductBuildDescription] + public let productMap: IdentifiableSet /// The plugin descriptions. Plugins are represented in the package graph /// as targets, but they are not directly included in the build graph. @@ -290,13 +290,12 @@ public class BuildPlan: SPMBuildCore.BuildPlan { var prebuildCommandResults: [ResolvedModule.ID: [CommandPluginResult]] = [:] // Create product description for each product we have in the package graph that is eligible. - var productMap: [ResolvedProduct.ID: (product: ResolvedProduct, buildDescription: ProductBuildDescription)] = - [:] + var productMap = IdentifiableSet() // Create build target description for each target which we need to plan. // Plugin targets are noted, since they need to be compiled, but they do // not get directly incorporated into the build description that will be // given to LLBuild. - var targetMap = [ResolvedModule.ID: ModuleBuildDescription]() + var targetMap = IdentifiableSet() var pluginDescriptions = [PluginBuildDescription]() var shouldGenerateTestObservation = true @@ -312,7 +311,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan { throw InternalError("Package not found for product: \(product.name)") } - productMap[product.id] = try (product, ProductBuildDescription( + try productMap.insert(ProductBuildDescription( package: package, product: product, toolsVersion: package.manifest.toolsVersion, @@ -384,7 +383,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan { shouldGenerateTestObservation = false // Only generate the code once. } - targetMap[module.id] = try .swift( + try targetMap.insert(.swift( SwiftModuleBuildDescription( package: package, target: module, @@ -399,9 +398,9 @@ public class BuildPlan: SPMBuildCore.BuildPlan { fileSystem: fileSystem, observabilityScope: planningObservabilityScope ) - ) + )) case is ClangModule: - targetMap[module.id] = try .clang( + try targetMap.insert(.clang( ClangModuleBuildDescription( package: package, target: module, @@ -413,7 +412,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan { fileSystem: fileSystem, observabilityScope: planningObservabilityScope ) - ) + )) case is PluginModule: try module.dependencies.compactMap { switch $0 { @@ -429,7 +428,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan { return nil } }.forEach { - productMap[$0.id] = try ($0, ProductBuildDescription( + try productMap.insert(ProductBuildDescription( package: package, product: $0, toolsVersion: package.manifest.toolsVersion, @@ -468,7 +467,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan { // Plan the derived test targets, if necessary. let derivedTestTargets = try Self.makeDerivedTestTargets( - testProducts: productMap.values.filter { + testProducts: productMap.filter { $0.product.type == .test }, destinationBuildParameters: destinationBuildParameters, @@ -480,12 +479,12 @@ public class BuildPlan: SPMBuildCore.BuildPlan { for item in derivedTestTargets { var derivedTestTargets = [item.entryPointTargetBuildDescription.target] - targetMap[item.entryPointTargetBuildDescription.target.id] = .swift( + targetMap.insert(.swift( item.entryPointTargetBuildDescription - ) + )) if let discoveryTargetBuildDescription = item.discoveryTargetBuildDescription { - targetMap[discoveryTargetBuildDescription.target.id] = .swift(discoveryTargetBuildDescription) + targetMap.insert(.swift(discoveryTargetBuildDescription)) derivedTestTargets.append(discoveryTargetBuildDescription.target) } @@ -495,7 +494,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan { self.buildToolPluginInvocationResults = buildToolPluginInvocationResults self.prebuildCommandResults = prebuildCommandResults - self.productMap = productMap.mapValues(\.buildDescription) + self.productMap = productMap self.targetMap = targetMap self.pluginDescriptions = pluginDescriptions @@ -706,6 +705,34 @@ public class BuildPlan: SPMBuildCore.BuildPlan { } return inputs } + + public func description( + for product: ResolvedProduct, + context: BuildParameters.Destination + ) -> ProductBuildDescription? { + let destination: BuildParameters.Destination = switch product.type { + case .macro, .plugin: + .host + default: + context + } + + return self.productMap[.init(productID: product.id, destination: destination)] + } + + public func description( + for module: ResolvedModule, + context: BuildParameters.Destination + ) -> ModuleBuildDescription? { + let destination: BuildParameters.Destination = switch module.type { + case .macro, .plugin: + .host + default: + context + } + + return self.targetMap[.init(moduleID: module.id, destination: destination)] + } } extension BuildPlan { @@ -986,9 +1013,9 @@ extension BuildPlan { } for module in package.modules { - if case .test = module.underlying.type, - !graph.rootPackages.contains(id: package.id) - { + // Tests are discovered through an aggregate product which also + // informs their destination. + if case .test = module.underlying.type { continue } @@ -1002,7 +1029,7 @@ extension BuildPlan { for product: ResolvedProduct, destination: Destination ) -> [TraversalNode] { - guard destination == .host else { + guard destination == .host || product.underlying.type == .test else { return [] } @@ -1117,6 +1144,64 @@ extension BuildPlan { } } } + + package func traverseDependencies( + of description: ModuleBuildDescription, + onProduct: (ResolvedProduct, BuildParameters.Destination, ProductBuildDescription?) -> Void, + onModule: (ResolvedModule, BuildParameters.Destination, ModuleBuildDescription?) -> Void + ) { + var visited = Set() + func successors( + for product: ResolvedProduct, + destination: Destination + ) -> [TraversalNode] { + product.modules.map { module in + TraversalNode(module: module, context: destination) + }.filter { + visited.insert($0).inserted + } + } + + func successors( + for module: ResolvedModule, + destination: Destination + ) -> [TraversalNode] { + module + .dependencies(satisfying: description.buildParameters.buildEnvironment) + .reduce(into: [TraversalNode]()) { partial, dependency in + switch dependency { + case .product(let product, _): + partial.append(.init(product: product, context: destination)) + case .module(let module, _): + partial.append(.init(module: module, context: destination)) + } + }.filter { + visited.insert($0).inserted + } + } + + depthFirstSearch(successors(for: description.module, destination: description.destination)) { + switch $0 { + case .module(let module, let destination): + successors(for: module, destination: destination) + case .product(let product, let destination): + successors(for: product, destination: destination) + case .package: + [] + } + } onNext: { module, _, _ in + switch module { + case .package: + break + + case .product(let product, let destination): + onProduct(product, destination, self.description(for: product, context: destination)) + + case .module(let module, let destination): + onModule(module, destination, self.description(for: module, context: destination)) + } + } + } } extension Basics.Diagnostic { diff --git a/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift b/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift index 89e486677a2..a95c69a53ce 100644 --- a/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift +++ b/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift @@ -38,7 +38,7 @@ public struct BuildParameters: Encodable { } /// The destination for which code should be compiled for. - public enum Destination: Encodable { + public enum Destination: Hashable, Encodable { /// The destination for which build tools are compiled. case host diff --git a/Sources/SourceKitLSPAPI/BuildDescription.swift b/Sources/SourceKitLSPAPI/BuildDescription.swift index 0c7db59fe79..6892917821e 100644 --- a/Sources/SourceKitLSPAPI/BuildDescription.swift +++ b/Sources/SourceKitLSPAPI/BuildDescription.swift @@ -15,7 +15,7 @@ import struct Foundation.URL private import struct Basics.AbsolutePath private import func Basics.resolveSymlinks -private import SPMBuildCore +internal import SPMBuildCore // FIXME: should import these module with `private` or `internal` access control import class Build.BuildPlan @@ -127,9 +127,12 @@ public struct BuildDescription { self.inputs = buildPlan.inputs } - // FIXME: should not use `ResolvedTarget` in the public interface - public func getBuildTarget(for target: ResolvedModule, in modulesGraph: ModulesGraph) -> BuildTarget? { - if let description = buildPlan.targetMap[target.id] { + func getBuildTarget( + for module: ResolvedModule, + destination: BuildParameters.Destination + ) -> BuildTarget? { + if let description = self.buildPlan.description(for: module, context: destination) { + let modulesGraph = self.buildPlan.graph switch description { case .clang(let description): return WrappedClangTargetBuildDescription( @@ -143,9 +146,10 @@ public struct BuildDescription { ) } } else { - if target.type == .plugin, let package = self.buildPlan.graph.package(for: target) { + if module.type == .plugin, let package = self.buildPlan.graph.package(for: module) { + let modulesGraph = self.buildPlan.graph return PluginTargetBuildDescription( - target: target, + target: module, toolsVersion: package.manifest.toolsVersion, toolchain: buildPlan.toolsBuildParameters.toolchain, isPartOfRootPackage: modulesGraph.rootPackages.map(\.id).contains(package.id) @@ -158,17 +162,14 @@ public struct BuildDescription { public func traverseModules( callback: (any BuildTarget, _ parent: (any BuildTarget)?, _ depth: Int) -> Void ) { - // TODO: Once the `targetMap` is switched over to use `IdentifiableSet` - // we can introduce `BuildPlan.description(ResolvedModule, BuildParameters.Destination)` - // and start using that here. self.buildPlan.traverseModules { module, parent, depth in let parentDescription: (any BuildTarget)? = if let parent { - getBuildTarget(for: parent.0, in: self.buildPlan.graph) + getBuildTarget(for: parent.0, destination: parent.1) } else { nil } - if let description = getBuildTarget(for: module.0, in: self.buildPlan.graph) { + if let description = getBuildTarget(for: module.0, destination: module.1) { callback(description, parentDescription, depth) } } diff --git a/Sources/_InternalTestSupport/MockBuildTestHelper.swift b/Sources/_InternalTestSupport/MockBuildTestHelper.swift index 94ca81d1ec0..30a68afc3a1 100644 --- a/Sources/_InternalTestSupport/MockBuildTestHelper.swift +++ b/Sources/_InternalTestSupport/MockBuildTestHelper.swift @@ -297,13 +297,7 @@ public struct BuildPlanResult { ) self.targetMap = try Dictionary( throwingUniqueKeysWithValues: plan.targetMap.compactMap { - guard - let target = plan.graph.allModules[$0] ?? - IdentifiableSet(plan.derivedTestTargetsMap.values.flatMap { $0 })[$0] - else { - throw BuildError.error("Target \($0) not found.") - } - return (target.id, $1) + ($0.module.id, $0) } ) } diff --git a/Sources/_InternalTestSupport/MockPackageGraphs.swift b/Sources/_InternalTestSupport/MockPackageGraphs.swift index ba576b84b4c..6d5a92a2f1e 100644 --- a/Sources/_InternalTestSupport/MockPackageGraphs.swift +++ b/Sources/_InternalTestSupport/MockPackageGraphs.swift @@ -137,6 +137,7 @@ package func macrosTestsPackageGraph() throws -> MockPackageGraph { "/swift-mmio/Sources/MMIOMacros/source.swift", "/swift-mmio/Sources/MMIOMacrosTests/source.swift", "/swift-mmio/Sources/MMIOMacro+PluginTests/source.swift", + "/swift-mmio/Sources/NOOPTests/source.swift", "/swift-syntax/Sources/SwiftSyntax/source.swift", "/swift-syntax/Sources/SwiftSyntaxMacrosTestSupport/source.swift", "/swift-syntax/Sources/SwiftSyntaxMacros/source.swift", @@ -203,6 +204,11 @@ package func macrosTestsPackageGraph() throws -> MockPackageGraph { .target(name: "MMIOMacros") ], type: .test + ), + TargetDescription( + name: "NOOPTests", + dependencies: [], + type: .test ) ] ), diff --git a/Tests/BuildTests/BuildPlanTraversalTests.swift b/Tests/BuildTests/BuildPlanTraversalTests.swift index 41777385362..1babf8f007d 100644 --- a/Tests/BuildTests/BuildPlanTraversalTests.swift +++ b/Tests/BuildTests/BuildPlanTraversalTests.swift @@ -143,4 +143,128 @@ final class BuildPlanTraversalTests: XCTestCase { XCTAssertEqual(self.getUniqueOccurrences(in: results, for: "SwiftSyntax", destination: .host), [2, 3, 4, 5, 6]) XCTAssertEqual(self.getUniqueOccurrences(in: results, for: "HAL"), [1, 2, 3]) } + + func testRecursiveDependencyTraversal() async throws { + let destinationTriple = Triple.arm64Linux + let toolsTriple = Triple.x86_64MacOS + + let (graph, fs, scope) = try macrosPackageGraph() + let plan = try await BuildPlan( + destinationBuildParameters: mockBuildParameters( + destination: .target, + triple: destinationTriple + ), + toolsBuildParameters: mockBuildParameters( + destination: .host, + triple: toolsTriple + ), + graph: graph, + fileSystem: fs, + observabilityScope: scope + ) + + let mmioModule = try XCTUnwrap(plan.description(for: graph.module(for: "MMIO")!, context: .target)) + + var moduleDependencies: [(ResolvedModule, Dest, Build.ModuleBuildDescription?)] = [] + plan.traverseDependencies(of: mmioModule) { product, destination, description in + XCTAssertEqual(product.name, "SwiftSyntax") + XCTAssertEqual(destination, .host) + XCTAssertNil(description) + } onModule: { module, destination, description in + moduleDependencies.append((module, destination, description)) + } + + XCTAssertEqual(moduleDependencies.count, 2) + + // The ordering is guaranteed by the traversal + + XCTAssertEqual(moduleDependencies[0].0.name, "MMIOMacros") + XCTAssertEqual(moduleDependencies[1].0.name, "SwiftSyntax") + + for index in 0 ..< moduleDependencies.count { + XCTAssertEqual(moduleDependencies[index].1, .host) + XCTAssertNotNil(moduleDependencies[index].2) + } + + let directDependencies = mmioModule.dependencies(using: plan) + + XCTAssertEqual(directDependencies.count, 1) + + let dependency = try XCTUnwrap(directDependencies.first) + if case .module(let module, let description) = dependency { + XCTAssertEqual(module.name, "MMIOMacros") + try XCTAssertEqual(XCTUnwrap(description).destination, .host) + } else { + XCTFail("Expected MMIOMacros module") + } + + let dependencies = mmioModule.recursiveDependencies(using: plan) + + XCTAssertEqual(dependencies.count, 3) + + // MMIOMacros (module) -> SwiftSyntax (product) -> SwiftSyntax (module) + + if case .module(let module, let description) = dependencies[0] { + XCTAssertEqual(module.name, "MMIOMacros") + try XCTAssertEqual(XCTUnwrap(description).destination, .host) + } else { + XCTFail("Expected MMIOMacros module") + } + + if case .product(let product, let description) = dependencies[1] { + XCTAssertEqual(product.name, "SwiftSyntax") + XCTAssertNil(description) + } else { + XCTFail("Expected SwiftSyntax product") + } + + if case .module(let module, let description) = dependencies[2] { + XCTAssertEqual(module.name, "SwiftSyntax") + try XCTAssertEqual(XCTUnwrap(description).destination, .host) + } else { + XCTFail("Expected SwiftSyntax module") + } + } + + func testRecursiveDependencyTraversalWithDuplicates() async throws { + let destinationTriple = Triple.arm64Linux + let toolsTriple = Triple.x86_64MacOS + + let (graph, fs, scope) = try macrosTestsPackageGraph() + let plan = try await BuildPlan( + destinationBuildParameters: mockBuildParameters( + destination: .target, + triple: destinationTriple + ), + toolsBuildParameters: mockBuildParameters( + destination: .host, + triple: toolsTriple + ), + graph: graph, + fileSystem: fs, + observabilityScope: scope + ) + + let testModule = try XCTUnwrap(plan.description(for: graph.module(for: "MMIOMacrosTests")!, context: .host)) + + let dependencies = testModule.recursiveDependencies(using: plan) + XCTAssertEqual(dependencies.count, 9) + + struct ModuleResult: Hashable { + let module: ResolvedModule + let destination: Dest + } + + var uniqueModules = Set() + for dependency in dependencies { + if case .module(let module, let description) = dependency { + XCTAssertNotNil(description) + XCTAssertEqual(description!.destination, .host) + XCTAssertTrue( + uniqueModules.insert(.init(module: module, destination: description!.destination)) + .inserted + ) + } + } + } } diff --git a/Tests/BuildTests/CrossCompilationBuildPlanTests.swift b/Tests/BuildTests/CrossCompilationBuildPlanTests.swift index d053b876987..607d1c8767e 100644 --- a/Tests/BuildTests/CrossCompilationBuildPlanTests.swift +++ b/Tests/BuildTests/CrossCompilationBuildPlanTests.swift @@ -295,9 +295,15 @@ final class CrossCompilationBuildPlanTests: XCTestCase { fileSystem: fs, observabilityScope: scope ) + + // Make sure that build plan doesn't have any "target" tests. + for description in plan.targetMap where description.module.underlying.type == .test { + XCTAssertEqual(description.buildParameters.destination, .host) + } + let result = try BuildPlanResult(plan: plan) result.checkProductsCount(2) - result.checkTargetsCount(16) + result.checkTargetsCount(17) XCTAssertTrue(try result.allTargets(named: "SwiftSyntax") .map { try $0.swift() } diff --git a/Tests/PackageGraphTests/CrossCompilationPackageGraphTests.swift b/Tests/PackageGraphTests/CrossCompilationPackageGraphTests.swift index c749ea28aba..790c62e114a 100644 --- a/Tests/PackageGraphTests/CrossCompilationPackageGraphTests.swift +++ b/Tests/PackageGraphTests/CrossCompilationPackageGraphTests.swift @@ -116,7 +116,11 @@ final class CrossCompilationPackageGraphTests: XCTestCase { "SwiftSyntaxMacrosTestSupport", "SwiftSyntaxMacrosTestSupport" ) - result.check(testModules: "MMIOMacrosTests", "MMIOMacro+PluginTests") + // TODO: NOOPTests are mentioned twice because in the graph they appear + // as if they target both "tools" and "destination", see the test below. + // Once the `buildTriple` is gone, there is going to be only one mention + // left. + result.check(testModules: "MMIOMacrosTests", "MMIOMacro+PluginTests", "NOOPTests", "NOOPTests") result.checkTarget("MMIO") { result in result.check(buildTriple: .destination) result.check(dependencies: "MMIOMacros") @@ -180,6 +184,13 @@ final class CrossCompilationPackageGraphTests: XCTestCase { XCTAssertEqual(graph.package(for: result.target)?.identity, .plain("swift-syntax")) } } + + result.checkTargets("NOOPTests") { results in + XCTAssertEqual(results.count, 2) + + XCTAssertEqual(results.filter({ $0.target.buildTriple == .tools }).count, 1) + XCTAssertEqual(results.filter({ $0.target.buildTriple == .destination }).count, 1) + } } } diff --git a/Tests/SourceKitLSPAPITests/SourceKitLSPAPITests.swift b/Tests/SourceKitLSPAPITests/SourceKitLSPAPITests.swift index 5c7eec517dd..399544d3d2d 100644 --- a/Tests/SourceKitLSPAPITests/SourceKitLSPAPITests.swift +++ b/Tests/SourceKitLSPAPITests/SourceKitLSPAPITests.swift @@ -17,7 +17,8 @@ import Build import PackageGraph import PackageModel -import SourceKitLSPAPI +@testable import SourceKitLSPAPI +import struct SPMBuildCore.BuildParameters import _InternalTestSupport import XCTest @@ -90,7 +91,7 @@ final class SourceKitLSPAPITests: XCTestCase { "-I", "/fake/manifestLib/path" ], isPartOfRootPackage: true, - destination: .tools + destination: .host ) } @@ -167,10 +168,10 @@ extension SourceKitLSPAPI.BuildDescription { graph: ModulesGraph, partialArguments: [String], isPartOfRootPackage: Bool, - destination: BuildTriple = .destination + destination: BuildParameters.Destination = .target ) throws -> Bool { - let target = try XCTUnwrap(graph.module(for: targetName, destination: destination)) - let buildTarget = try XCTUnwrap(self.getBuildTarget(for: target, in: graph)) + let target = try XCTUnwrap(graph.module(for: targetName)) + let buildTarget = try XCTUnwrap(self.getBuildTarget(for: target, destination: destination)) guard let file = buildTarget.sources.first else { XCTFail("build target \(targetName) contains no files")