diff --git a/Fixtures/Traits/Example/Package.swift b/Fixtures/Traits/Example/Package.swift new file mode 100644 index 00000000000..53df4f0d16c --- /dev/null +++ b/Fixtures/Traits/Example/Package.swift @@ -0,0 +1,103 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "TraitsExample", + traits: [ + Trait(name: "Package1", isDefault: true), + Trait(name: "Package2", isDefault: true), + Trait(name: "Package3", isDefault: true), + Trait(name: "Package4", isDefault: true), + "Package5", + "Package7", + "Package9", + "Package10", + Trait(name: "BuildCondition1", isDefault: true), + "BuildCondition2", + "BuildCondition3", + ], + dependencies: [ + .package( + path: "../Package1", + traits: ["Package1Trait1"] + ), + .package( + path: "../Package2", + traits: ["Package2Trait1"] + ), + .package( + path: "../Package3" + ), + .package( + path: "../Package4", + traits: [] + ), + .package( + path: "../Package5", + traits: ["Package5Trait1"] + ), + .package( + path: "../Package7" + ), + .package( + path: "../Package9" + ), + .package( + path: "../Package10", + traits: ["Package10Trait2"] + ), + ], + targets: [ + .executableTarget( + name: "Example", + dependencies: [ + .product( + name: "Package1Library1", + package: "Package1", + condition: .when(traits: ["Package1"]) + ), + .product( + name: "Package2Library1", + package: "Package2", + condition: .when(traits: ["Package2"]) + ), + .product( + name: "Package3Library1", + package: "Package3", + condition: .when(traits: ["Package3"]) + ), + .product( + name: "Package4Library1", + package: "Package4", + condition: .when(traits: ["Package4"]) + ), + .product( + name: "Package5Library1", + package: "Package5", + condition: .when(traits: ["Package5"]) + ), + .product( + name: "Package7Library1", + package: "Package7", + condition: .when(traits: ["Package7"]) + ), + .product( + name: "Package9Library1", + package: "Package9", + condition: .when(traits: ["Package9"]) + ), + .product( + name: "Package10Library1", + package: "Package10", + condition: .when(traits: ["Package10"]) + ), + ], + swiftSettings: [ + .define("DEFINE1", .when(traits: ["BuildCondition1"])), + .define("DEFINE2", .when(traits: ["BuildCondition2"])), + .define("DEFINE3", .when(traits: ["BuildCondition3"])), + ] + ), + ] +) diff --git a/Fixtures/Traits/Example/Sources/Example/Example.swift b/Fixtures/Traits/Example/Sources/Example/Example.swift new file mode 100644 index 00000000000..5f1d871df50 --- /dev/null +++ b/Fixtures/Traits/Example/Sources/Example/Example.swift @@ -0,0 +1,69 @@ +#if Package1 +import Package1Library1 +#endif +#if Package2 +import Package2Library1 +#endif +#if Package3 +import Package3Library1 +#endif +#if Package4 +import Package4Library1 +#endif +#if Package5 +import Package5Library1 +#endif +#if Package7 +import Package7Library1 +#endif +#if Package9 +import Package9Library1 +#endif +#if Package10 +import Package10Library1 +#endif + +@main +struct Example { + static func main() { + #if Package1 + Package1Library1.hello() + #endif + #if Package2 + Package2Library1.hello() + #endif + #if Package3 + Package3Library1.hello() + #endif + #if Package4 + Package4Library1.hello() + #endif + #if Package5 + Package5Library1.hello() + #endif + #if Package7 + Package7Library1.hello() + #endif + #if Package9 + Package9Library1.hello() + #endif + #if Package10 + Package10Library1.hello() + #endif + #if DEFINE1 + print("DEFINE1 enabled") + #else + print("DEFINE1 disabled") + #endif + #if DEFINE2 + print("DEFINE2 enabled") + #else + print("DEFINE2 disabled") + #endif + #if DEFINE3 + print("DEFINE3 enabled") + #else + print("DEFINE3 disabled") + #endif + } +} diff --git a/Fixtures/Traits/Package1/Package.swift b/Fixtures/Traits/Package1/Package.swift new file mode 100644 index 00000000000..500ef1c273e --- /dev/null +++ b/Fixtures/Traits/Package1/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package1", + products: [ + .library( + name: "Package1Library1", + targets: ["Package1Library1"] + ), + ], + traits: [ + "Package1Trait1" + ], + targets: [ + .target( + name: "Package1Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package1/Sources/Package1Library1/Library.swift b/Fixtures/Traits/Package1/Sources/Package1Library1/Library.swift new file mode 100644 index 00000000000..f5497881355 --- /dev/null +++ b/Fixtures/Traits/Package1/Sources/Package1Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package1Trait1 + print("Package1Library1 trait1 enabled") + #else + print("Package1Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package10/Package.swift b/Fixtures/Traits/Package10/Package.swift new file mode 100644 index 00000000000..9f9ae186d28 --- /dev/null +++ b/Fixtures/Traits/Package10/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package10", + products: [ + .library( + name: "Package10Library1", + targets: ["Package10Library1"] + ), + ], + traits: [ + "Package10Trait1", + "Package10Trait2" + ], + targets: [ + .target( + name: "Package10Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package10/Sources/Package10Library1/Library.swift b/Fixtures/Traits/Package10/Sources/Package10Library1/Library.swift new file mode 100644 index 00000000000..febb8c54e0e --- /dev/null +++ b/Fixtures/Traits/Package10/Sources/Package10Library1/Library.swift @@ -0,0 +1,12 @@ +public func hello() { + #if Package10Trait1 + print("Package10Library1 trait1 enabled") + #else + print("Package10Library1 trait1 disabled") + #endif + #if Package10Trait2 + print("Package10Library1 trait2 enabled") + #else + print("Package10Library1 trait2 disabled") + #endif +} diff --git a/Fixtures/Traits/Package2/Package.swift b/Fixtures/Traits/Package2/Package.swift new file mode 100644 index 00000000000..33bda8a6db3 --- /dev/null +++ b/Fixtures/Traits/Package2/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package2", + products: [ + .library( + name: "Package2Library1", + targets: ["Package2Library1"] + ), + ], + traits: [ + Trait(name: "Package2Trait1", enabledTraits: ["Package2Trait2"]), + "Package2Trait2", + ], + targets: [ + .target( + name: "Package2Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package2/Sources/Package2Library1/Library.swift b/Fixtures/Traits/Package2/Sources/Package2Library1/Library.swift new file mode 100644 index 00000000000..42e2bbbedb7 --- /dev/null +++ b/Fixtures/Traits/Package2/Sources/Package2Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package2Trait2 + print("Package2Library1 trait2 enabled") + #else + print("Package2Library1 trait2 disabled") + #endif +} diff --git a/Fixtures/Traits/Package3/Package.swift b/Fixtures/Traits/Package3/Package.swift new file mode 100644 index 00000000000..33cc8455e89 --- /dev/null +++ b/Fixtures/Traits/Package3/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package3", + products: [ + .library( + name: "Package3Library1", + targets: ["Package3Library1"] + ), + ], + traits: [ + Trait(name: "Package3Trait1", enabledTraits: ["Package3Trait2"]), + Trait(name: "Package3Trait2", enabledTraits: ["Package3Trait3"]), + Trait(name: "Package3Trait3", isDefault: true), + ], + targets: [ + .target( + name: "Package3Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package3/Sources/Package3Library1/Library.swift b/Fixtures/Traits/Package3/Sources/Package3Library1/Library.swift new file mode 100644 index 00000000000..041bab2a1df --- /dev/null +++ b/Fixtures/Traits/Package3/Sources/Package3Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package3Trait3 + print("Package3Library1 trait3 enabled") + #else + print("Package3Library1 trait3 disabled") + #endif +} diff --git a/Fixtures/Traits/Package4/Package.swift b/Fixtures/Traits/Package4/Package.swift new file mode 100644 index 00000000000..50e30743b40 --- /dev/null +++ b/Fixtures/Traits/Package4/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package4", + products: [ + .library( + name: "Package4Library1", + targets: ["Package4Library1"] + ), + ], + traits: [ + Trait(name: "Package4Trait1", isDefault: true), + ], + targets: [ + .target( + name: "Package4Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package4/Sources/Package4Library1/Library.swift b/Fixtures/Traits/Package4/Sources/Package4Library1/Library.swift new file mode 100644 index 00000000000..c79fd748e7b --- /dev/null +++ b/Fixtures/Traits/Package4/Sources/Package4Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package4Trait1 + print("Package4Library1 trait1 enabled") + #else + print("Package4Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package5/Package.swift b/Fixtures/Traits/Package5/Package.swift new file mode 100644 index 00000000000..e2612ee4c94 --- /dev/null +++ b/Fixtures/Traits/Package5/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package5", + products: [ + .library( + name: "Package5Library1", + targets: ["Package5Library1"] + ), + ], + traits: [ + "Package5Trait1" + ], + dependencies: [ + .package( + path: "../Package6", + traits: [ + Package.Dependency.Trait(name: "Package6Trait1", condition: .when(traits: ["Package5Trait1"])) + ] + ) + ], + targets: [ + .target( + name: "Package5Library1", + dependencies: [ + .product( + name: "Package6Library1", + package: "Package6" + ) + ] + ), + ] +) diff --git a/Fixtures/Traits/Package5/Sources/Package5Library1/Library.swift b/Fixtures/Traits/Package5/Sources/Package5Library1/Library.swift new file mode 100644 index 00000000000..4547568225d --- /dev/null +++ b/Fixtures/Traits/Package5/Sources/Package5Library1/Library.swift @@ -0,0 +1,12 @@ +#if Package5Trait1 +import Package6Library1 +#endif + +public func hello() { + #if Package5Trait1 + print("Package5Library1 trait1 enabled") + Package6Library1.hello() + #else + print("Package5Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package6/Package.swift b/Fixtures/Traits/Package6/Package.swift new file mode 100644 index 00000000000..b73ed6961bd --- /dev/null +++ b/Fixtures/Traits/Package6/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package6", + products: [ + .library( + name: "Package6Library1", + targets: ["Package6Library1"] + ), + ], + traits: [ + "Package6Trait1" + ], + targets: [ + .target( + name: "Package6Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package6/Sources/Package6Library1/Library.swift b/Fixtures/Traits/Package6/Sources/Package6Library1/Library.swift new file mode 100644 index 00000000000..f92779bbd78 --- /dev/null +++ b/Fixtures/Traits/Package6/Sources/Package6Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package6Trait1 + print("Package6Library1 trait1 enabled") + #else + print("Package6Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package7/Package.swift b/Fixtures/Traits/Package7/Package.swift new file mode 100644 index 00000000000..6f3995c36f0 --- /dev/null +++ b/Fixtures/Traits/Package7/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package7", + products: [ + .library( + name: "Package7Library1", + targets: ["Package7Library1"] + ), + ], + traits: [ + "Package7Trait1" + ], + dependencies: [ + .package( + path: "../Package8" + ) + ], + targets: [ + .target( + name: "Package7Library1", + dependencies: [ + .product( + name: "Package8Library1", + package: "Package8", + condition: .when(traits: ["Package7Trait1"]) + ) + ] + ), + ] +) diff --git a/Fixtures/Traits/Package7/Sources/Package7Library1/Library.swift b/Fixtures/Traits/Package7/Sources/Package7Library1/Library.swift new file mode 100644 index 00000000000..fb74b92a69a --- /dev/null +++ b/Fixtures/Traits/Package7/Sources/Package7Library1/Library.swift @@ -0,0 +1,12 @@ +#if Package7Trait1 +import Package8Library1 +#endif + +public func hello() { + #if Package7Trait1 + print("Package7Library1 trait1 enabled") + Package8Library1.hello() + #else + print("Package7Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package8/Package.swift b/Fixtures/Traits/Package8/Package.swift new file mode 100644 index 00000000000..ffd295c3975 --- /dev/null +++ b/Fixtures/Traits/Package8/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package8", + products: [ + .library( + name: "Package8Library1", + targets: ["Package8Library1"] + ), + ], + traits: [ + "Package8Trait1" + ], + targets: [ + .target( + name: "Package8Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package8/Sources/Package6Library1/Library.swift b/Fixtures/Traits/Package8/Sources/Package6Library1/Library.swift new file mode 100644 index 00000000000..bc5427d75a0 --- /dev/null +++ b/Fixtures/Traits/Package8/Sources/Package6Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package8Trait1 + print("Package8Library1 trait1 enabled") + #else + print("Package8Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package9/Package.swift b/Fixtures/Traits/Package9/Package.swift new file mode 100644 index 00000000000..4735ddc3aaa --- /dev/null +++ b/Fixtures/Traits/Package9/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 999.0.0 + +@_spi(ExperimentalTraits) import PackageDescription + +let package = Package( + name: "Package9", + products: [ + .library( + name: "Package9Library1", + targets: ["Package9Library1"] + ), + ], + dependencies: [ + .package( + path: "../Package10", + traits: ["Package10Trait1"] + ) + ], + targets: [ + .target( + name: "Package9Library1", + dependencies: [ + .product( + name: "Package10Library1", + package: "Package10" + ) + ] + ), + ] +) diff --git a/Fixtures/Traits/Package9/Sources/Package9Library1/Library.swift b/Fixtures/Traits/Package9/Sources/Package9Library1/Library.swift new file mode 100644 index 00000000000..44f8d0ead95 --- /dev/null +++ b/Fixtures/Traits/Package9/Sources/Package9Library1/Library.swift @@ -0,0 +1,5 @@ +import Package10Library1 + +public func hello() { + Package10Library1.hello() +} diff --git a/Sources/PackageDescription/PackageDependency.swift b/Sources/PackageDescription/PackageDependency.swift index 24b3faddeb2..e9e6f8cabcb 100644 --- a/Sources/PackageDescription/PackageDependency.swift +++ b/Sources/PackageDescription/PackageDependency.swift @@ -53,7 +53,7 @@ extension Package { /// The dependencies traits configuration. @_spi(ExperimentalTraits) @available(_PackageDescription, introduced: 999.0) - public let traits: Set? + public let traits: Set /// The name of the dependency. /// @@ -145,7 +145,7 @@ extension Package { init(kind: Kind, traits: Set?) { self.kind = kind - self.traits = traits + self.traits = traits ?? [.defaults] } convenience init( diff --git a/Sources/PackageDescription/PackageDescriptionSerializationConversion.swift b/Sources/PackageDescription/PackageDescriptionSerializationConversion.swift index 48af2243754..8f20f027918 100644 --- a/Sources/PackageDescription/PackageDescriptionSerializationConversion.swift +++ b/Sources/PackageDescription/PackageDescriptionSerializationConversion.swift @@ -172,7 +172,7 @@ extension Serialization.PackageDependency { init(_ dependency: PackageDescription.Package.Dependency) { self.kind = .init(dependency.kind) self.moduleAliases = dependency.moduleAliases - self.traits = Set(dependency.traits?.map { Serialization.PackageDependency.Trait.init($0) } ?? []) + self.traits = Set(dependency.traits.map { Serialization.PackageDependency.Trait.init($0) }) } } diff --git a/Sources/PackageGraph/GraphLoadingNode.swift b/Sources/PackageGraph/GraphLoadingNode.swift index 7c66c751be7..f35cca2ac2c 100644 --- a/Sources/PackageGraph/GraphLoadingNode.swift +++ b/Sources/PackageGraph/GraphLoadingNode.swift @@ -29,10 +29,54 @@ public struct GraphLoadingNode: Equatable, Hashable { /// The product filter applied to the package. public let productFilter: ProductFilter - public init(identity: PackageIdentity, manifest: Manifest, productFilter: ProductFilter) { + /// The enabled traits for this package. + package var enabledTraits: Set + + public init( + identity: PackageIdentity, + manifest: Manifest, + productFilter: ProductFilter, + enabledTraits: Set? + ) throws { self.identity = identity self.manifest = manifest self.productFilter = productFilter + + // This the point where we flatten the enabled traits and resolve the recursive traits + var recursiveEnabledTraits = enabledTraits ?? [] + let areDefaultsEnabled = recursiveEnabledTraits.remove("defaults") != nil + + // We are going to calculate which traits are actually enabled for a node here. To do this + // we have to check if default traits should be used and then flatten all the enabled traits. + for trait in recursiveEnabledTraits { + // Check if the enabled trait is a valid trait + if self.manifest.traits.first(where: { $0.name == trait }) == nil { + // The enabled trait is invalid + throw ModuleError.invalidTrait(package: identity, trait: trait) + } + } + + // We have to enable all default traits if no traits are enabled or the defaults are explicitly enabled + if enabledTraits == nil || areDefaultsEnabled { + recursiveEnabledTraits.formUnion(self.manifest.traits.lazy.filter { $0.isDefault }.map { $0.name }) + } + + while true { + let flattendEnabledTraits = Set(self.manifest.traits + .lazy + .filter { recursiveEnabledTraits.contains($0.name) } + .map { $0.enabledTraits } + .joined() + ) + let newRecursiveEnabledTraits = recursiveEnabledTraits.union(flattendEnabledTraits) + if newRecursiveEnabledTraits.count == recursiveEnabledTraits.count { + break + } else { + recursiveEnabledTraits = newRecursiveEnabledTraits + } + } + + self.enabledTraits = recursiveEnabledTraits } /// Returns the dependencies required by this node. @@ -51,7 +95,3 @@ extension GraphLoadingNode: CustomStringConvertible { } } } - -extension GraphLoadingNode: Identifiable { - public var id: PackageIdentity { self.identity } -} diff --git a/Sources/PackageGraph/ModulesGraph+Loading.swift b/Sources/PackageGraph/ModulesGraph+Loading.swift index 085e118be2e..f50a4c67850 100644 --- a/Sources/PackageGraph/ModulesGraph+Loading.swift +++ b/Sources/PackageGraph/ModulesGraph+Loading.swift @@ -37,7 +37,6 @@ extension ModulesGraph { fileSystem: FileSystem, observabilityScope: ObservabilityScope ) throws -> ModulesGraph { - let observabilityScope = observabilityScope.makeChildScope(description: "Loading Package Graph") // Create a map of the manifests, keyed by their identity. @@ -51,43 +50,64 @@ extension ModulesGraph { let rootDependencies = Set(root.dependencies.compactMap{ manifestMap[$0.identity]?.manifest }) - let rootManifestNodes = root.packages.map { identity, package in - GraphLoadingNode(identity: identity, manifest: package.manifest, productFilter: .everything) + + let rootManifestNodes = try root.packages.map { identity, package in + // We are going to enable all default traits of the root manifests. + // In a future PR we make this overridable by CLI options. + let enabledTraits = package.manifest.traits.lazy.filter { $0.isDefault }.map { $0.name } + return try GraphLoadingNode( + identity: identity, + manifest: package.manifest, + productFilter: .everything, + enabledTraits: Set(enabledTraits) + ) } - let rootDependencyNodes = root.dependencies.lazy.compactMap { dependency in - manifestMap[dependency.identity].map { - GraphLoadingNode( + let rootDependencyNodes = try root.dependencies.lazy.compactMap { dependency in + try manifestMap[dependency.identity].map { + try GraphLoadingNode( identity: dependency.identity, manifest: $0.manifest, - productFilter: dependency.productFilter + productFilter: dependency.productFilter, + enabledTraits: dependency.traits.flatMap { Set($0.map { $0.name }) } ) } } let inputManifests = (rootManifestNodes + rootDependencyNodes).map { - KeyedPair($0, key: $0.id) + KeyedPair($0, key: $0.identity) } // Collect the manifests for which we are going to build packages. - var allNodes = [GraphLoadingNode]() + var allNodes = OrderedDictionary() let nodeSuccessorProvider = { (node: KeyedPair) in - node.item.requiredDependencies.compactMap { dependency in - manifestMap[dependency.identity].map { manifest, _ in - KeyedPair( - GraphLoadingNode( - identity: dependency.identity, - manifest: manifest, - productFilter: dependency.productFilter - ), - key: dependency.identity - ) + return try node.item.requiredDependencies.compactMap { dependency in + return try manifestMap[dependency.identity].map { manifest, _ in + // We are going to check the conditionally enabled traits here and enable them if + // required. This checks the current node and then enables the conditional + // dependencies of the dependency node. + let enabledTraits = dependency.traits?.filter { + guard let conditionTraits = $0.condition?.traits else { + return true + } + return !conditionTraits.intersection(node.item.enabledTraits).isEmpty + }.map { $0.name } + + return try KeyedPair( + GraphLoadingNode( + identity: dependency.identity, + manifest: manifest, + productFilter: dependency.productFilter, + enabledTraits: enabledTraits.flatMap { Set($0) } + ), + key: dependency.identity + ) } } } // Package dependency cycles feature is gated on tools version 6.0. if !root.manifests.allSatisfy({ $1.toolsVersion >= .v6_0 }) { - if let cycle = findCycle(inputManifests, successors: nodeSuccessorProvider) { + if let cycle = try findCycle(inputManifests, successors: nodeSuccessorProvider) { let path = (cycle.path + cycle.cycle).map(\.item.manifest) observabilityScope.emit(PackageGraphError.dependencyCycleDetected( path: path, cycle: cycle.cycle[0].item.manifest @@ -104,18 +124,19 @@ extension ModulesGraph { } // Cycles in dependencies don't matter as long as there are no target cycles between packages. - depthFirstSearch( + try depthFirstSearch( inputManifests, successors: nodeSuccessorProvider ) { - allNodes.append($0.item) - } onDuplicate: { _,_ in - // no de-duplication is required. + allNodes[$0.key] = $0.item + } onDuplicate: { first, second in + // We are unifying the enabled traits on duplicate + allNodes[first.key]?.enabledTraits.formUnion(second.item.enabledTraits) } // Create the packages. var manifestToPackage: [Manifest: Package] = [:] - for node in allNodes { + for node in allNodes.values { let nodeObservabilityScope = observabilityScope.makeChildScope( description: "loading package \(node.identity)", metadata: .packageMetadata(identity: node.identity, kind: node.manifest.packageKind) @@ -139,7 +160,8 @@ extension ModulesGraph { testEntryPointPath: testEntryPointPath, createREPLProduct: manifest.packageKind.isRoot ? createREPLProduct : false, fileSystem: fileSystem, - observabilityScope: nodeObservabilityScope + observabilityScope: nodeObservabilityScope, + enabledTraits: node.enabledTraits ) let package = try builder.construct() manifestToPackage[manifest] = package @@ -162,7 +184,7 @@ extension ModulesGraph { // Resolve dependencies and create resolved packages. let resolvedPackages = try createResolvedPackages( - nodes: allNodes, + nodes: Array(allNodes.values), identityResolver: identityResolver, manifestToPackage: manifestToPackage, rootManifests: root.manifests, @@ -273,7 +295,7 @@ private func createResolvedPackages( ) throws -> IdentifiableSet { // Create package builder objects from the input manifests. - let packageBuilders: [ResolvedPackageBuilder] = nodes.compactMap{ node in + let packageBuilders: [ResolvedPackageBuilder] = nodes.compactMap { node in guard let package = manifestToPackage[node.manifest] else { return nil } @@ -283,6 +305,7 @@ private func createResolvedPackages( return ResolvedPackageBuilder( package, productFilter: node.productFilter, + enabledTraits: node.enabledTraits, isAllowedToVendUnsafeProducts: isAllowedToVendUnsafeProducts, allowedToOverride: allowedToOverride, platformVersionProvider: platformVersionProvider @@ -315,8 +338,9 @@ private func createResolvedPackages( var dependenciesByNameForTargetDependencyResolution = [String: ResolvedPackageBuilder]() var dependencyNamesForTargetDependencyResolutionOnly = [PackageIdentity: String]() - // Establish the manifest-declared package dependencies. - package.manifest.dependenciesRequired(for: packageBuilder.productFilter).forEach { dependency in + package.manifest.dependenciesRequired( + for: packageBuilder.productFilter + ).forEach { dependency in let dependencyPackageRef = dependency.packageRef // Otherwise, look it up by its identity. @@ -530,6 +554,13 @@ private func createResolvedPackages( // Establish product dependencies. for case .product(let productRef, let conditions) in targetBuilder.target.dependencies { + if let traitCondition = conditions.compactMap({ $0.traitCondition }).first { + if packageBuilder.enabledTraits.intersection(traitCondition.traits).isEmpty { + /// If we land here non of the traits required to enable this depenendcy has been enabled. + continue + } + } + // Find the product in this package's dependency products. // Look it up by ID if module aliasing is used, otherwise by name. let product = lookupByProductIDs ? productDependencyMap[productRef.identity] : productDependencyMap[productRef.name] @@ -1054,6 +1085,9 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { /// The products in this package. var products: [ResolvedProductBuilder] = [] + /// The enabled traits of this package. + var enabledTraits: Set = [] + /// The dependencies of this package. var dependencies: [ResolvedPackageBuilder] = [] @@ -1074,12 +1108,14 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { init( _ package: Package, productFilter: ProductFilter, + enabledTraits: Set, isAllowedToVendUnsafeProducts: Bool, allowedToOverride: Bool, platformVersionProvider: PlatformVersionProvider ) { self.package = package self.productFilter = productFilter + self.enabledTraits = enabledTraits self.isAllowedToVendUnsafeProducts = isAllowedToVendUnsafeProducts self.allowedToOverride = allowedToOverride self.platformVersionProvider = platformVersionProvider @@ -1095,6 +1131,7 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { defaultLocalization: self.defaultLocalization, supportedPlatforms: self.supportedPlatforms, dependencies: self.dependencies.map { $0.package.identity }, + enabledTraits: self.enabledTraits, targets: targets, products: products, registryMetadata: self.registryMetadata, diff --git a/Sources/PackageGraph/Resolution/ResolvedPackage.swift b/Sources/PackageGraph/Resolution/ResolvedPackage.swift index 32536f0dbb1..74e26915560 100644 --- a/Sources/PackageGraph/Resolution/ResolvedPackage.swift +++ b/Sources/PackageGraph/Resolution/ResolvedPackage.swift @@ -39,6 +39,9 @@ public struct ResolvedPackage { /// The products produced by the package. public let products: [ResolvedProduct] + /// The enabled traits of this package. + package let enabledTraits: Set + /// The dependencies of the package. public let dependencies: [PackageIdentity] @@ -58,6 +61,7 @@ public struct ResolvedPackage { defaultLocalization: String?, supportedPlatforms: [SupportedPlatform], dependencies: [PackageIdentity], + enabledTraits: Set, targets: IdentifiableSet, products: [ResolvedProduct], registryMetadata: RegistryReleaseMetadata?, @@ -71,6 +75,7 @@ public struct ResolvedPackage { self.supportedPlatforms = supportedPlatforms self.registryMetadata = registryMetadata self.platformVersionProvider = platformVersionProvider + self.enabledTraits = enabledTraits } public func getSupportedPlatform(for platform: Platform, usingXCTest: Bool) -> SupportedPlatform { diff --git a/Sources/PackageLoading/ManifestJSONParser.swift b/Sources/PackageLoading/ManifestJSONParser.swift index bbe950b9e15..8088436a525 100644 --- a/Sources/PackageLoading/ManifestJSONParser.swift +++ b/Sources/PackageLoading/ManifestJSONParser.swift @@ -638,7 +638,7 @@ extension MappablePackageDependency { path: path ), productFilter: .everything, - traits: Set(seed.traits?.map { PackageDependency.Trait.init($0) } ?? []) + traits: seed.traits.flatMap { Set($0.map { PackageDependency.Trait.init($0) } ) } ) case .sourceControl(let name, let location, let requirement): self.init( @@ -649,7 +649,7 @@ extension MappablePackageDependency { requirement: .init(requirement) ), productFilter: .everything, - traits: Set(seed.traits?.map { PackageDependency.Trait.init($0) } ?? []) + traits: seed.traits.flatMap { Set($0.map { PackageDependency.Trait.init($0) } ) } ) case .registry(let id, let requirement): self.init( @@ -659,7 +659,7 @@ extension MappablePackageDependency { requirement: .init(requirement) ), productFilter: .everything, - traits: Set(seed.traits?.map { PackageDependency.Trait.init($0) } ?? []) + traits: seed.traits.flatMap { Set($0.map { PackageDependency.Trait.init($0) } ) } ) } } diff --git a/Sources/PackageLoading/PackageBuilder.swift b/Sources/PackageLoading/PackageBuilder.swift index 6f5f17f9563..ed527a1d952 100644 --- a/Sources/PackageLoading/PackageBuilder.swift +++ b/Sources/PackageLoading/PackageBuilder.swift @@ -91,6 +91,12 @@ public enum ModuleError: Swift.Error { scmPackage: PackageIdentity, targets: [String] ) + + /// Indicates that an invalid trait was enabled. + case invalidTrait( + package: PackageIdentity, + trait: String + ) } extension ModuleError: CustomStringConvertible { @@ -169,6 +175,10 @@ extension ModuleError: CustomStringConvertible { by activating the automatic source-control to registry replacement, or by using mirrors. \ if they are not duplicate consider using the `moduleAliases` parameter in manifest to provide unique names """ + case .invalidTrait(let package, let trait): + return """ + Trait '"\(trait)"' is not declared by package '\(package)'. + """ } } } @@ -321,6 +331,9 @@ public final class PackageBuilder { // Caches the version we chose to build for. private var swiftVersionCache: SwiftLanguageVersion? = nil + /// The enabled traits of this package. + private let enabledTraits: Set + /// Create a builder for the given manifest and package `path`. /// /// - Parameters: @@ -343,7 +356,8 @@ public final class PackageBuilder { warnAboutImplicitExecutableTargets: Bool = true, createREPLProduct: Bool = false, fileSystem: FileSystem, - observabilityScope: ObservabilityScope + observabilityScope: ObservabilityScope, + enabledTraits: Set ) { self.identity = identity self.manifest = manifest @@ -360,6 +374,7 @@ public final class PackageBuilder { metadata: .packageMetadata(identity: self.identity, kind: self.manifest.packageKind) ) self.fileSystem = fileSystem + self.enabledTraits = enabledTraits } /// Build a new package following the conventions. @@ -1069,6 +1084,11 @@ public final class PackageBuilder { // Process each setting. for setting in target.settings { + if let traits = setting.condition?.traits, traits.intersection(self.enabledTraits).isEmpty { + // The setting is currently not enabled so we should skip it + continue + } + let decl: BuildSettings.Declaration let values: [String] @@ -1198,6 +1218,14 @@ public final class PackageBuilder { table.add(assignment, for: decl) } + // For each trait we are now generating an additional define + for trait in self.enabledTraits { + var assignment = BuildSettings.Assignment() + assignment.values = ["\(trait)"] + assignment.conditions = [] + table.add(assignment, for: .SWIFT_ACTIVE_COMPILATION_CONDITIONS) + } + return table } @@ -1218,6 +1246,10 @@ public final class PackageBuilder { conditions.append(.init(platforms: platforms)) } + if let traits = condition?.traits { + conditions.append(.traits(.init(traits: traits))) + } + return conditions } diff --git a/Sources/PackageModel/DependencyMapper.swift b/Sources/PackageModel/DependencyMapper.swift index 838398bb465..2d0245e12df 100644 --- a/Sources/PackageModel/DependencyMapper.swift +++ b/Sources/PackageModel/DependencyMapper.swift @@ -133,13 +133,13 @@ public struct MappablePackageDependency { public let parentPackagePath: AbsolutePath public let kind: Kind public let productFilter: ProductFilter - public let traits: Set + package let traits: Set? - public init( + package init( parentPackagePath: AbsolutePath, kind: Kind, productFilter: ProductFilter, - traits: Set + traits: Set? ) { self.parentPackagePath = parentPackagePath self.kind = kind @@ -147,6 +147,19 @@ public struct MappablePackageDependency { self.traits = traits } + public init( + parentPackagePath: AbsolutePath, + kind: Kind, + productFilter: ProductFilter + ) { + self.init( + parentPackagePath: parentPackagePath, + kind: kind, + productFilter: productFilter, + traits: nil + ) + } + public enum Kind { case fileSystem(name: String?, path: String) case sourceControl(name: String?, location: String, requirement: PackageDependency.SourceControl.Requirement) diff --git a/Sources/PackageModel/Manifest/PackageDependencyDescription.swift b/Sources/PackageModel/Manifest/PackageDependencyDescription.swift index df0bfb477bd..4feb8e1db99 100644 --- a/Sources/PackageModel/Manifest/PackageDependencyDescription.swift +++ b/Sources/PackageModel/Manifest/PackageDependencyDescription.swift @@ -20,11 +20,11 @@ import struct TSCUtility.Version /// Represents a package dependency. public enum PackageDependency: Equatable, Hashable, Sendable { /// A struct representing an enabled trait of a dependency. - public struct Trait: Hashable, Sendable, Codable, ExpressibleByStringLiteral { + package struct Trait: Hashable, Sendable, Codable, ExpressibleByStringLiteral { /// A condition that limits the application of a dependencies trait. - public struct Condition: Hashable, Sendable, Codable { + package struct Condition: Hashable, Sendable, Codable { /// The set of traits of this package that enable the dependencie's trait. - let traits: Set? + package let traits: Set? public init(traits: Set?) { self.traits = traits @@ -32,17 +32,17 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } /// The name of the enabled trait. - public var name: String + package var name: String /// The condition under which the trait is enabled. - public var condition: Condition? + package var condition: Condition? /// Initializes a new enabled trait. /// /// - Parameters: /// - name: The name of the enabled trait. /// - condition: The condition under which the trait is enabled. - public init( + package init( name: String, condition: Condition? = nil ) { @@ -59,7 +59,7 @@ public enum PackageDependency: Equatable, Hashable, Sendable { /// - Parameters: /// - name: The name of the enabled trait. /// - condition: The condition under which the trait is enabled. - public static func trait( + package static func trait( name: String, condition: Condition? = nil ) -> Trait { @@ -79,7 +79,7 @@ public enum PackageDependency: Equatable, Hashable, Sendable { public let nameForTargetDependencyResolutionOnly: String? public let path: AbsolutePath public let productFilter: ProductFilter - public let traits: Set + package let traits: Set? } public struct SourceControl: Equatable, Hashable, Encodable, Sendable { @@ -88,7 +88,7 @@ public enum PackageDependency: Equatable, Hashable, Sendable { public let location: Location public let requirement: Requirement public let productFilter: ProductFilter - public let traits: Set + package let traits: Set? public enum Requirement: Equatable, Hashable, Sendable { case exact(Version) @@ -107,7 +107,7 @@ public enum PackageDependency: Equatable, Hashable, Sendable { public let identity: PackageIdentity public let requirement: Requirement public let productFilter: ProductFilter - public let traits: Set + package let traits: Set? /// The dependency requirement. public enum Requirement: Equatable, Hashable, Sendable { @@ -116,7 +116,7 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } } - public var traits: Set { + package var traits: Set? { switch self { case .fileSystem(let settings): return settings.traits @@ -210,11 +210,26 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } public static func fileSystem( + identity: PackageIdentity, + nameForTargetDependencyResolutionOnly: String?, + path: AbsolutePath, + productFilter: ProductFilter + ) -> Self { + .fileSystem( + identity: identity, + nameForTargetDependencyResolutionOnly: nameForTargetDependencyResolutionOnly, + path: path, + productFilter: productFilter, + traits: nil + ) + } + + package static func fileSystem( identity: PackageIdentity, nameForTargetDependencyResolutionOnly: String?, path: AbsolutePath, productFilter: ProductFilter, - traits: Set + traits: Set? ) -> Self { .fileSystem( .init( @@ -228,12 +243,29 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } public static func localSourceControl( + identity: PackageIdentity, + nameForTargetDependencyResolutionOnly: String?, + path: AbsolutePath, + requirement: SourceControl.Requirement, + productFilter: ProductFilter + ) -> Self { + .localSourceControl( + identity: identity, + nameForTargetDependencyResolutionOnly: nameForTargetDependencyResolutionOnly, + path: path, + requirement: requirement, + productFilter: productFilter, + traits: nil + ) + } + + package static func localSourceControl( identity: PackageIdentity, nameForTargetDependencyResolutionOnly: String?, path: AbsolutePath, requirement: SourceControl.Requirement, productFilter: ProductFilter, - traits: Set + traits: Set? ) -> Self { .sourceControl( identity: identity, @@ -246,12 +278,29 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } public static func remoteSourceControl( + identity: PackageIdentity, + nameForTargetDependencyResolutionOnly: String?, + url: SourceControlURL, + requirement: SourceControl.Requirement, + productFilter: ProductFilter + ) -> Self { + .remoteSourceControl( + identity: identity, + nameForTargetDependencyResolutionOnly: nameForTargetDependencyResolutionOnly, + url: url, + requirement: requirement, + productFilter: productFilter, + traits: nil + ) + } + + package static func remoteSourceControl( identity: PackageIdentity, nameForTargetDependencyResolutionOnly: String?, url: SourceControlURL, requirement: SourceControl.Requirement, productFilter: ProductFilter, - traits: Set + traits: Set? ) -> Self { .sourceControl( identity: identity, @@ -264,12 +313,29 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } public static func sourceControl( + identity: PackageIdentity, + nameForTargetDependencyResolutionOnly: String?, + location: SourceControl.Location, + requirement: SourceControl.Requirement, + productFilter: ProductFilter + ) -> Self { + .sourceControl( + identity: identity, + nameForTargetDependencyResolutionOnly: nameForTargetDependencyResolutionOnly, + location: location, + requirement: requirement, + productFilter: productFilter, + traits: nil + ) + } + + package static func sourceControl( identity: PackageIdentity, nameForTargetDependencyResolutionOnly: String?, location: SourceControl.Location, requirement: SourceControl.Requirement, productFilter: ProductFilter, - traits: Set + traits: Set? ) -> Self { .sourceControl( .init( @@ -284,10 +350,23 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } public static func registry( + identity: PackageIdentity, + requirement: Registry.Requirement, + productFilter: ProductFilter + ) -> Self { + .registry( + identity: identity, + requirement: requirement, + productFilter: productFilter, + traits: nil + ) + } + + package static func registry( identity: PackageIdentity, requirement: Registry.Requirement, productFilter: ProductFilter, - traits: Set + traits: Set? ) -> Self { .registry( .init( diff --git a/Sources/SPMTestSupport/ManifestExtensions.swift b/Sources/SPMTestSupport/ManifestExtensions.swift index b8e815eb548..13268bbac57 100644 --- a/Sources/SPMTestSupport/ManifestExtensions.swift +++ b/Sources/SPMTestSupport/ManifestExtensions.swift @@ -31,7 +31,8 @@ extension Manifest { swiftLanguageVersions: [SwiftLanguageVersion]? = nil, dependencies: [PackageDependency] = [], products: [ProductDescription] = [], - targets: [TargetDescription] = [] + targets: [TargetDescription] = [], + traits: Set = [] ) -> Manifest { Self.createManifest( displayName: displayName, @@ -49,7 +50,8 @@ extension Manifest { swiftLanguageVersions: swiftLanguageVersions, dependencies: dependencies, products: products, - targets: targets + targets: targets, + traits: traits ) } @@ -67,7 +69,8 @@ extension Manifest { swiftLanguageVersions: [SwiftLanguageVersion]? = nil, dependencies: [PackageDependency] = [], products: [ProductDescription] = [], - targets: [TargetDescription] = [] + targets: [TargetDescription] = [], + traits: Set = [] ) -> Manifest { Self.createManifest( displayName: displayName, @@ -85,7 +88,8 @@ extension Manifest { swiftLanguageVersions: swiftLanguageVersions, dependencies: dependencies, products: products, - targets: targets + targets: targets, + traits: traits ) } diff --git a/Sources/SPMTestSupport/MockDependency.swift b/Sources/SPMTestSupport/MockDependency.swift index c5b95ed3fff..f0e59a78070 100644 --- a/Sources/SPMTestSupport/MockDependency.swift +++ b/Sources/SPMTestSupport/MockDependency.swift @@ -22,7 +22,7 @@ public struct MockDependency { public let deprecatedName: String? public let location: Location public let products: ProductFilter - public let traits: Set + package let traits: Set init( deprecatedName: String? = nil, diff --git a/Sources/SPMTestSupport/MockPackage.swift b/Sources/SPMTestSupport/MockPackage.swift index d692b883930..5ec767c415f 100644 --- a/Sources/SPMTestSupport/MockPackage.swift +++ b/Sources/SPMTestSupport/MockPackage.swift @@ -22,6 +22,7 @@ public struct MockPackage { public let products: [MockProduct] public let dependencies: [MockDependency] public let versions: [String?] + package let traits: Set /// Provides revision identifier for the given version. A random identifier might be assigned if this is nil. public let revisionProvider: ((String) -> String)? // FIXME: This should be per-version. @@ -34,6 +35,7 @@ public struct MockPackage { targets: [MockTarget], products: [MockProduct] = [], dependencies: [MockDependency] = [], + traits: Set = [], versions: [String?] = [], revisionProvider: ((String) -> String)? = nil, toolsVersion: ToolsVersion? = nil @@ -45,6 +47,7 @@ public struct MockPackage { self.targets = targets self.products = products self.dependencies = dependencies + self.traits = traits self.versions = versions self.revisionProvider = revisionProvider self.toolsVersion = toolsVersion @@ -57,6 +60,7 @@ public struct MockPackage { targets: [MockTarget], products: [MockProduct], dependencies: [MockDependency] = [], + traits: Set = [], versions: [String?] = [], revisionProvider: ((String) -> String)? = nil, toolsVersion: ToolsVersion? = nil @@ -67,6 +71,7 @@ public struct MockPackage { self.targets = targets self.products = products self.dependencies = dependencies + self.traits = traits self.versions = versions self.revisionProvider = revisionProvider self.toolsVersion = toolsVersion @@ -81,6 +86,7 @@ public struct MockPackage { targets: [MockTarget], products: [MockProduct], dependencies: [MockDependency] = [], + traits: Set = [], versions: [String?] = [], revisionProvider: ((String) -> String)? = nil, toolsVersion: ToolsVersion? = nil @@ -95,6 +101,7 @@ public struct MockPackage { self.targets = targets self.products = products self.dependencies = dependencies + self.traits = traits self.versions = versions self.revisionProvider = revisionProvider self.toolsVersion = toolsVersion diff --git a/Sources/SPMTestSupport/MockWorkspace.swift b/Sources/SPMTestSupport/MockWorkspace.swift index ad37a6ee890..4e53ec634e9 100644 --- a/Sources/SPMTestSupport/MockWorkspace.swift +++ b/Sources/SPMTestSupport/MockWorkspace.swift @@ -265,7 +265,8 @@ public final class MockWorkspace { toolsVersion: packageToolsVersion, dependencies: package.dependencies.map { try $0.convert(baseURL: packagesDir, identityResolver: self.identityResolver) }, products: package.products.map { try ProductDescription(name: $0.name, type: .library(.automatic), targets: $0.targets) }, - targets: try package.targets.map { try $0.convert(identityResolver: self.identityResolver) } + targets: try package.targets.map { try $0.convert(identityResolver: self.identityResolver) }, + traits: package.traits ) } diff --git a/Sources/SPMTestSupport/PackageDependencyDescriptionExtensions.swift b/Sources/SPMTestSupport/PackageDependencyDescriptionExtensions.swift index f470ad09f5e..a29ece267b0 100644 --- a/Sources/SPMTestSupport/PackageDependencyDescriptionExtensions.swift +++ b/Sources/SPMTestSupport/PackageDependencyDescriptionExtensions.swift @@ -16,13 +16,13 @@ import PackageModel import struct TSCUtility.Version -public extension PackageDependency { +package extension PackageDependency { static func fileSystem( identity: PackageIdentity? = nil, deprecatedName: String? = nil, path: AbsolutePath, productFilter: ProductFilter = .everything, - traits: Set = [] + traits: Set = [.init(name: "defaults")] ) -> Self { let identity = identity ?? PackageIdentity(path: path) return .fileSystem( @@ -40,7 +40,7 @@ public extension PackageDependency { path: AbsolutePath, requirement: SourceControl.Requirement, productFilter: ProductFilter = .everything, - traits: Set = [] + traits: Set = [.init(name: "defaults")] ) -> Self { let identity = identity ?? PackageIdentity(path: path) return .localSourceControl( @@ -59,7 +59,7 @@ public extension PackageDependency { url: SourceControlURL, requirement: SourceControl.Requirement, productFilter: ProductFilter = .everything, - traits: Set = [] + traits: Set = [.init(name: "defaults")] ) -> Self { let identity = identity ?? PackageIdentity(url: url) return .remoteSourceControl( @@ -76,7 +76,7 @@ public extension PackageDependency { identity: String, requirement: Registry.Requirement, productFilter: ProductFilter = .everything, - traits: Set = [] + traits: Set = [.init(name: "defaults")] ) -> Self { return .registry( identity: .plain(identity), diff --git a/Sources/SPMTestSupport/PackageGraphTester.swift b/Sources/SPMTestSupport/PackageGraphTester.swift index f8d370b7238..b6234ac545f 100644 --- a/Sources/SPMTestSupport/PackageGraphTester.swift +++ b/Sources/SPMTestSupport/PackageGraphTester.swift @@ -127,6 +127,18 @@ public final class PackageGraphResult { body(ResolvedProductResult(product)) } + package func checkPackage( + _ name: String, + file: StaticString = #file, + line: UInt = #line, + body: (ResolvedPackage) -> Void + ) { + guard let package = find(package: .init(stringLiteral: name)) else { + return XCTFail("Product \(name) not found", file: file, line: line) + } + body(package) + } + public func check(testModules: String..., file: StaticString = #file, line: UInt = #line) { XCTAssertEqual( graph.allTargets @@ -217,6 +229,19 @@ public final class ResolvedTargetResult { XCTAssertEqual(platform.options, options, file: file, line: line) } + package func checkBuildSetting( + declaration: BuildSettings.Declaration, + assignments: Set?, + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssertEqual( + target.underlying.buildSettings.assignments[declaration].flatMap { Set($0) }, + assignments, + file: file, + line: line + ) + } public func check(buildTriple: BuildTriple, file: StaticString = #file, line: UInt = #line) { XCTAssertEqual(self.target.buildTriple, buildTriple, file: file, line: line) } diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 8b48dea8df2..b1fde04a014 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -200,22 +200,26 @@ extension Workspace { }) var inputIdentities: OrderedCollections.OrderedSet = [] - let inputNodes: [GraphLoadingNode] = root.packages.map { identity, package in + let inputNodes: [GraphLoadingNode] = try root.packages.map { identity, package in inputIdentities.append(package.reference) - let node = GraphLoadingNode( + let node = try GraphLoadingNode( identity: identity, manifest: package.manifest, - productFilter: .everything + productFilter: .everything, + // We are enabling all traits of the root packages in the workspace integration for now + enabledTraits: Set(package.manifest.traits.map { $0.name }) ) return node } + root.dependencies.compactMap { dependency in let package = dependency.packageRef inputIdentities.append(package) - return manifestsMap[dependency.identity].map { manifest in - GraphLoadingNode( + return try manifestsMap[dependency.identity].map { manifest in + try GraphLoadingNode( identity: dependency.identity, manifest: manifest, - productFilter: dependency.productFilter + productFilter: dependency.productFilter, + // We are enabling all traits of the root packages in the workspace integration for now + enabledTraits: Set(manifest.traits.map { $0.name }) ) } } @@ -223,8 +227,8 @@ extension Workspace { let topLevelDependencies = root.packages.flatMap { $1.manifest.dependencies.map(\.packageRef) } var requiredIdentities: OrderedCollections.OrderedSet = [] - _ = transitiveClosure(inputNodes) { node in - node.manifest.dependenciesRequired(for: node.productFilter).compactMap { dependency in + _ = try transitiveClosure(inputNodes) { node in + try node.manifest.dependenciesRequired(for: node.productFilter).compactMap { dependency in let package = dependency.packageRef let (inserted, index) = requiredIdentities.append(package) if !inserted { @@ -265,11 +269,13 @@ extension Workspace { """) } } - return manifestsMap[dependency.identity].map { manifest in - GraphLoadingNode( + return try manifestsMap[dependency.identity].map { manifest in + try GraphLoadingNode( identity: dependency.identity, manifest: manifest, - productFilter: dependency.productFilter + productFilter: dependency.productFilter, + // We are enabling all traits of the root packages in the workspace integration for now + enabledTraits: Set(manifest.traits.map { $0.name }) ) } } diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index 8d462a0b7ff..b3a39688140 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -1108,7 +1108,9 @@ extension Workspace { additionalFileRules: [], binaryArtifacts: binaryArtifacts, fileSystem: self.fileSystem, - observabilityScope: observabilityScope + observabilityScope: observabilityScope, + // For now we enable all traits + enabledTraits: Set(manifest.traits.map { $0.name }) ) return try builder.construct() } @@ -1188,7 +1190,9 @@ extension Workspace { shouldCreateMultipleTestProducts: self.configuration.shouldCreateMultipleTestProducts, createREPLProduct: self.configuration.createREPLProduct, fileSystem: self.fileSystem, - observabilityScope: observabilityScope + observabilityScope: observabilityScope, + // For now we enable all traits + enabledTraits: Set(manifest.traits.map { $0.name }) ) return try builder.construct() } diff --git a/Tests/BuildTests/ClangTargetBuildDescriptionTests.swift b/Tests/BuildTests/ClangTargetBuildDescriptionTests.swift index 6c14a23c7d1..5408fe15e0a 100644 --- a/Tests/BuildTests/ClangTargetBuildDescriptionTests.swift +++ b/Tests/BuildTests/ClangTargetBuildDescriptionTests.swift @@ -106,6 +106,7 @@ final class ClangTargetBuildDescriptionTests: XCTestCase { defaultLocalization: nil, supportedPlatforms: [], dependencies: [], + enabledTraits: [], targets: .init([target]), products: [], registryMetadata: nil, diff --git a/Tests/FunctionalTests/TraitTests.swift b/Tests/FunctionalTests/TraitTests.swift new file mode 100644 index 00000000000..3331954008f --- /dev/null +++ b/Tests/FunctionalTests/TraitTests.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// +#if compiler(>=6.0) +import DriverSupport +import SPMTestSupport +import PackageModel +import XCTest + +final class TraitTests: XCTestCase { + func testTraits_whenNoFlagPassed() throws { + try fixture(name: "Traits") { fixturePath in + let (stdout, _) = try executeSwiftRun(fixturePath.appending("Example"), "Example") + XCTAssertEqual(stdout, """ + Package1Library1 trait1 enabled + Package2Library1 trait2 enabled + Package3Library1 trait3 enabled + Package4Library1 trait1 disabled + DEFINE1 enabled + DEFINE2 disabled + DEFINE3 disabled + + """) + } + } +} +#endif diff --git a/Tests/PackageGraphTests/ModulesGraphTests.swift b/Tests/PackageGraphTests/ModulesGraphTests.swift index 7b9c2314d34..87b73803385 100644 --- a/Tests/PackageGraphTests/ModulesGraphTests.swift +++ b/Tests/PackageGraphTests/ModulesGraphTests.swift @@ -2899,6 +2899,410 @@ final class ModulesGraphTests: XCTestCase { ) } } + + func testTraits_whenSingleManifest_andDefaultTrait() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Foo/Sources/Foo/source.swift" + ) + + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Foo", + path: "/Foo", + toolsVersion: .v5_9, + targets: [ + TargetDescription( + name: "Foo" + ), + ], + traits: [ + .init(name: "Trait1", isDefault: true), + "Trait1", + ] + ), + ], + observabilityScope: observability.topScope + ) + + XCTAssertEqual(observability.diagnostics.count, 0) + + PackageGraphTester(graph) { result in + result.checkPackage("Foo") { package in + XCTAssertEqual(package.enabledTraits, ["Trait1"]) + } + } + } + + func testTraits_whenTraitEnablesOtherTraits() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Foo/Sources/Foo/source.swift" + ) + + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Foo", + path: "/Foo", + toolsVersion: .v5_9, + targets: [ + TargetDescription( + name: "Foo" + ), + ], + traits: [ + .init(name: "Trait1", isDefault: true, enabledTraits: ["Trait2"]), + .init(name: "Trait2", enabledTraits: ["Trait3", "Trait4"]), + "Trait3", + .init(name: "Trait4", enabledTraits: ["Trait5"]), + "Trait5", + ] + ), + ], + observabilityScope: observability.topScope + ) + + XCTAssertEqual(observability.diagnostics.count, 0) + + PackageGraphTester(graph) { result in + result.checkPackage("Foo") { package in + XCTAssertEqual(package.enabledTraits, ["Trait1", "Trait2", "Trait3", "Trait4", "Trait5"]) + } + } + } + + func testTraits_whenDependencyTraitEnabled() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Package1/Sources/Package1Target1/source.swift", + "/Package2/Sources/Package2Target1/source.swift" + ) + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Package1", + path: "/Package1", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package2", + requirement: .upToNextMajor(from: "1.0.0"), + traits: ["Package2Trait1"] + ) + ], + targets: [ + TargetDescription( + name: "Package1Target1", + dependencies: [ + .product(name: "Package2Target1", package: "Package2") + ] + ), + ], + traits: [ + .init(name: "Package1Trait1", isDefault: true), + "Package1Trait1", + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package2", + path: "/Package2", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package2Target1", + type: .library(.automatic), + targets: ["Package2Target1"] + ) + ], + targets: [ + TargetDescription( + name: "Package2Target1" + ), + ], + traits: [ + "Package2Trait1" + ] + ), + ], + observabilityScope: observability.topScope + ) + + XCTAssertEqual(observability.diagnostics.count, 0) + + PackageGraphTester(graph) { result in + result.checkPackage("Package1") { package in + XCTAssertEqual(package.enabledTraits, ["Package1Trait1"]) + XCTAssertEqual(package.dependencies.count, 1) + } + result.checkPackage("Package2") { package in + XCTAssertEqual(package.enabledTraits, ["Package2Trait1"]) + } + } + } + + func testTraits_whenTraitEnablesDependencyTrait() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Package1/Sources/Package1Target1/source.swift", + "/Package2/Sources/Package2Target1/source.swift" + ) + + let manifests = try [ + Manifest.createRootManifest( + displayName: "Package1", + path: "/Package1", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package2", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) + ) + ], + targets: [ + TargetDescription( + name: "Package1Target1", + dependencies: [ + .product(name: "Package2Target1", package: "Package2") + ] + ), + ], + traits: [ + .init(name: "Package1Trait1", isDefault: true) + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package2", + path: "/Package2", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package2Target1", + type: .library(.automatic), + targets: ["Package2Target1"] + ) + ], + targets: [ + TargetDescription( + name: "Package2Target1" + ), + ], + traits: [ + "Package2Trait1" + ] + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope + ) + + XCTAssertEqual(observability.diagnostics.count, 0) + + PackageGraphTester(graph) { result in + result.checkPackage("Package1") { package in + XCTAssertEqual(package.enabledTraits, ["Package1Trait1"]) + XCTAssertEqual(package.dependencies.count, 1) + } + result.checkPackage("Package2") { package in + XCTAssertEqual(package.enabledTraits, ["Package2Trait1"]) + } + } + } + + func testTraits_whenComplex() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Package1/Sources/Package1Target1/source.swift", + "/Package2/Sources/Package2Target1/source.swift", + "/Package3/Sources/Package3Target1/source.swift", + "/Package4/Sources/Package4Target1/source.swift", + "/Package5/Sources/Package5Target1/source.swift" + ) + + let manifests = try [ + Manifest.createRootManifest( + displayName: "Package1", + path: "/Package1", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package2", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) + ), + .localSourceControl( + path: "/Package4", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init(["Package4Trait2"]) + ), + .localSourceControl( + path: "/Package5", + requirement: .upToNextMajor(from: "1.0.0") + ) + ], + targets: [ + TargetDescription( + name: "Package1Target1", + dependencies: [ + .product(name: "Package2Target1", package: "Package2"), + .product(name: "Package4Target1", package: "Package4"), + .product(name: "Package5Target1", package: "Package5", condition: .init(traits: ["Package1Trait2"])) + ], + settings: [ + .init( + tool: .swift, + kind: .define("TEST_DEFINE"), + condition: .init(traits: ["Package1Trait1"]) + ) + ] + ), + ], + traits: [ + .init(name: "Package1Trait1", isDefault: true), + .init(name: "Package1Trait2", isDefault: true), + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package2", + path: "/Package2", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package3", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package3Trait1", condition: .init(traits: ["Package2Trait1"]))]) + ) + ], + products: [ + .init( + name: "Package2Target1", + type: .library(.automatic), + targets: ["Package2Target1"] + ) + ], + targets: [ + TargetDescription( + name: "Package2Target1", + dependencies: [ + .product(name: "Package3Target1", package: "Package3") + ] + ), + ], + traits: [ + "Package2Trait1" + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package3", + path: "/Package3", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package4", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package4Trait1", condition: .init(traits: ["Package3Trait1"]))]) + ) + ], + products: [ + .init( + name: "Package3Target1", + type: .library(.automatic), + targets: ["Package3Target1"] + ) + ], + targets: [ + TargetDescription( + name: "Package3Target1", + dependencies: [ + .product(name: "Package4Target1", package: "Package4") + ] + ), + ], + traits: [ + "Package3Trait1" + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package4", + path: "/Package4", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package4Target1", + type: .library(.automatic), + targets: ["Package4Target1"] + ) + ], + targets: [ + TargetDescription( + name: "Package4Target1" + ), + ], + traits: [ + "Package4Trait1", + "Package4Trait2", + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package5", + path: "/Package5", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package5Target1", + type: .library(.automatic), + targets: ["Package5Target1"] + ) + ], + targets: [ + TargetDescription( + name: "Package5Target1" + ), + ] + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope + ) + + XCTAssertEqual(observability.diagnostics.count, 0) + + PackageGraphTester(graph) { result in + result.checkPackage("Package1") { package in + XCTAssertEqual(package.enabledTraits, ["Package1Trait1", "Package1Trait2"]) + XCTAssertEqual(package.dependencies.count, 3) + } + result.checkTarget("Package1Target1") { target in + target.check(dependencies: "Package2Target1", "Package4Target1", "Package5Target1") + target.checkBuildSetting( + declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, + assignments: [ + .init(values: ["TEST_DEFINE"], conditions: [.traits(.init(traits: ["Package1Trait1"]))]), + .init(values: ["Package1Trait2"]), + .init(values: ["Package1Trait1"]), + ] + ) + } + result.checkPackage("Package2") { package in + XCTAssertEqual(package.enabledTraits, ["Package2Trait1"]) + } + result.checkPackage("Package3") { package in + XCTAssertEqual(package.enabledTraits, ["Package3Trait1"]) + } + result.checkPackage("Package4") { package in + XCTAssertEqual(package.enabledTraits, ["Package4Trait1", "Package4Trait2"]) + } + } + } } diff --git a/Tests/PackageLoadingTests/PackageBuilderTests.swift b/Tests/PackageLoadingTests/PackageBuilderTests.swift index d42e401f7c7..23a3a4bbb83 100644 --- a/Tests/PackageLoadingTests/PackageBuilderTests.swift +++ b/Tests/PackageLoadingTests/PackageBuilderTests.swift @@ -3137,7 +3137,8 @@ final class PackageBuilderTester { warnAboutImplicitExecutableTargets: true, createREPLProduct: createREPLProduct, fileSystem: fs, - observabilityScope: observability.topScope + observabilityScope: observability.topScope, + enabledTraits: [] ) let loadedPackage = try builder.construct() self.result = .package(loadedPackage)