From 9579ccc31ed3a2a1a66119cd6cc0f2927cf9ca3d Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 21 Oct 2025 14:50:51 -0400 Subject: [PATCH 01/14] Fix for required deps calculation with traits The enabled traits property was essentially being erased to defaults rather than propagating what was calculated earlier due to how we were returning the DependencyResolutionNode during the pub grub stage. This fix should now assure that we are passing the appropriate enabled traits to these nodes, and are being considered when making calls to PackageContainer's dependency-related methods. --- .../PackageModel+Extensions.swift | 4 ++-- .../PubGrub/PubGrubDependencyResolver.swift | 5 +++-- .../Workspace/Workspace+Dependencies.swift | 19 ++++++++++++++++++- Sources/Workspace/Workspace+Manifests.swift | 15 +++++++++++++++ Sources/Workspace/Workspace+Traits.swift | 17 +++++++++++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 Sources/Workspace/Workspace+Traits.swift diff --git a/Sources/PackageGraph/PackageModel+Extensions.swift b/Sources/PackageGraph/PackageModel+Extensions.swift index 19f94f929b0..fbb9888039b 100644 --- a/Sources/PackageGraph/PackageModel+Extensions.swift +++ b/Sources/PackageGraph/PackageModel+Extensions.swift @@ -60,7 +60,7 @@ extension PackageContainerConstraint { internal func nodes() -> [DependencyResolutionNode] { switch products { case .everything: - return [.root(package: self.package)] + return [.root(package: self.package, enabledTraits: self.enabledTraits)] case .specific: switch products { case .everything: @@ -70,7 +70,7 @@ extension PackageContainerConstraint { if set.isEmpty { // Pointing at the package without a particular product. return [.empty(package: self.package)] } else { - return set.sorted().map { .product($0, package: self.package) } + return set.sorted().map { .product($0, package: self.package, enabledTraits: self.enabledTraits) } } } } diff --git a/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift b/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift index 801162636d8..b78be2fae5b 100644 --- a/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift +++ b/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift @@ -356,8 +356,9 @@ public struct PubGrubDependencyResolver { ) } + // TODO bp must update how we fetch dependencies wrt traits? for dependency in try await container.underlying - .getUnversionedDependencies(productFilter: node.productFilter, constraint.enabledTraits) + .getUnversionedDependencies(productFilter: node.productFilter, node.enabledTraits) // TODO bp replace constraint.enabledTraits with node.enabledTraits { if let versionedBasedConstraints = VersionBasedConstraint.constraints(dependency) { for constraint in versionedBasedConstraints { @@ -431,7 +432,7 @@ public struct PubGrubDependencyResolver { var unprocessedDependencies = try await container.underlying.getDependencies( at: revisionForDependencies, productFilter: constraint.products, - constraint.enabledTraits + node.enabledTraits // TODO bp replace constraint.enabledTraits with node.enabledTraits ) if let sharedRevision = node.revisionLock(revision: revision) { unprocessedDependencies.append(sharedRevision) diff --git a/Sources/Workspace/Workspace+Dependencies.swift b/Sources/Workspace/Workspace+Dependencies.swift index 64eb37227b8..816946b72af 100644 --- a/Sources/Workspace/Workspace+Dependencies.swift +++ b/Sources/Workspace/Workspace+Dependencies.swift @@ -507,6 +507,7 @@ extension Workspace { // Ensure the cache path exists and validate that edited dependencies. self.createCacheDirectories(observabilityScope: observabilityScope) + // Load the root manifests and currently checked out manifests. let rootManifests = try await self.loadRootManifests( packages: root.packages, @@ -515,6 +516,15 @@ extension Workspace { let rootManifestsMinimumToolsVersion = rootManifests.values.map(\.toolsVersion).min() ?? ToolsVersion.current let resolvedFileOriginHash = try self.computeResolvedFileOriginHash(root: root) + // Precompute enabled traits, beginning with + // root manifests, if we haven't already done so. + if self.enabledTraitsMap.dictionaryLiteral.isEmpty { + let rootManifestMap = rootManifests.values.reduce(into: [PackageIdentity: Manifest]()) { manifestMap, manifest in + manifestMap[manifest.packageIdentity] = manifest + } + self.enabledTraitsMap = .init(try precomputeTraits(rootManifests.values.map({ $0 }), rootManifestMap)) + } + // Load the current manifests. let graphRoot = try PackageGraphRoot( input: root, @@ -525,12 +535,15 @@ extension Workspace { enabledTraitsMap: self.enabledTraitsMap ) - // Of the enabled dependencies of targets, only consider these for dependency resolution let currentManifests = try await self.loadDependencyManifests( root: graphRoot, observabilityScope: observabilityScope ) + // Update traits map here; before we make call to + // resolveDependencies below, which will check out + // the depenedencies we need. + guard !observabilityScope.errorsReported else { return currentManifests } @@ -593,11 +606,15 @@ extension Workspace { } } + try await self.updateEnabledTraitsMap() + // Create the constraints; filter unused dependencies. var computedConstraints = [PackageContainerConstraint]() computedConstraints += currentManifests.editedPackagesConstraints computedConstraints += try graphRoot.constraints(self.enabledTraitsMap) + constraints + manifestLoader + // Perform dependency resolution. let resolver = try self.createResolver(resolvedPackages: resolvedPackagesStore.resolvedPackages, observabilityScope: observabilityScope) self.activeResolver = resolver diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 0a1f5c95c88..4cf45e75ad8 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -624,17 +624,27 @@ extension Workspace { >] = { node in // optimization: preload manifest we know about in parallel // avoid loading dependencies that are trait-guarded here since this is redundant. + // load conditional traits, if any let dependenciesRequired = try node.item.manifest.dependenciesRequired( for: node.item.productFilter, node.item.enabledTraits ) + if node.item.identity.description.contains("apple-configuration") { + print("enabled traits for apple config: \(node.item.enabledTraits)") + } let dependenciesToLoad = dependenciesRequired.map(\.packageRef) .filter { !loadedManifests.keys.contains($0.identity) } + if node.item.identity.description.contains("apple-configuration") { + print("dependencies to load: \(dependenciesToLoad.map(\.identity.description))") + } try await prepopulateManagedDependencies(dependenciesToLoad) let dependenciesManifests = await self.loadManagedManifests( for: dependenciesToLoad, observabilityScope: observabilityScope ) + if node.item.identity.description.contains("apple-configuration") { + print("dependencies manifests loaded: \(dependenciesManifests.map(\.key.description))") + } dependenciesManifests.forEach { loadedManifests[$0.key] = $0.value } return try dependenciesRequired.compactMap { dependency in return try loadedManifests[dependency.identity].flatMap { manifest in @@ -730,6 +740,7 @@ extension Workspace { dependencies.append((node.manifest, dependency, node.productFilter, fileSystem ?? self.fileSystem)) } + // dependency manifests returned here return DependencyManifests( root: root, dependencies: dependencies, @@ -962,6 +973,10 @@ extension Workspace { diagnostics: manifestLoadingDiagnostics, duration: duration ) + // update enabled traits map here + // Look for conditionally enabled traits in the manifest. +// let enabledTraits = manifest.enabledTraits(using: self.enabledTraitsMap[manifest.packageIdentity]) + return manifest } diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift new file mode 100644 index 00000000000..40043699eb5 --- /dev/null +++ b/Sources/Workspace/Workspace+Traits.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +extension Workspace { + package func updateEnabledTraitsMap() { + + } +} From 6e524b6fd18468731f9941bf8819f830e2611118 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Wed, 22 Oct 2025 11:33:23 -0400 Subject: [PATCH 02/14] Cleanup --- .../PubGrub/PubGrubDependencyResolver.swift | 5 +- Sources/Workspace/CMakeLists.txt | 3 +- .../Workspace/Workspace+Dependencies.swift | 7 --- Sources/Workspace/Workspace+Manifests.swift | 61 ------------------- Sources/Workspace/Workspace+Traits.swift | 53 +++++++++++++++- 5 files changed, 55 insertions(+), 74 deletions(-) diff --git a/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift b/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift index b78be2fae5b..a8ae0399409 100644 --- a/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift +++ b/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift @@ -356,9 +356,8 @@ public struct PubGrubDependencyResolver { ) } - // TODO bp must update how we fetch dependencies wrt traits? for dependency in try await container.underlying - .getUnversionedDependencies(productFilter: node.productFilter, node.enabledTraits) // TODO bp replace constraint.enabledTraits with node.enabledTraits + .getUnversionedDependencies(productFilter: node.productFilter, node.enabledTraits) { if let versionedBasedConstraints = VersionBasedConstraint.constraints(dependency) { for constraint in versionedBasedConstraints { @@ -432,7 +431,7 @@ public struct PubGrubDependencyResolver { var unprocessedDependencies = try await container.underlying.getDependencies( at: revisionForDependencies, productFilter: constraint.products, - node.enabledTraits // TODO bp replace constraint.enabledTraits with node.enabledTraits + node.enabledTraits ) if let sharedRevision = node.revisionLock(revision: revision) { unprocessedDependencies.append(sharedRevision) diff --git a/Sources/Workspace/CMakeLists.txt b/Sources/Workspace/CMakeLists.txt index d0b033eb1cf..cdc939112f2 100644 --- a/Sources/Workspace/CMakeLists.txt +++ b/Sources/Workspace/CMakeLists.txt @@ -40,7 +40,8 @@ add_library(Workspace Workspace+ResolvedPackages.swift Workspace+Signing.swift Workspace+SourceControl.swift - Workspace+State.swift) + Workspace+State.swift + Workspace+Traits.swift) target_link_libraries(Workspace PUBLIC TSCBasic TSCUtility diff --git a/Sources/Workspace/Workspace+Dependencies.swift b/Sources/Workspace/Workspace+Dependencies.swift index 816946b72af..592329e88ab 100644 --- a/Sources/Workspace/Workspace+Dependencies.swift +++ b/Sources/Workspace/Workspace+Dependencies.swift @@ -540,10 +540,6 @@ extension Workspace { observabilityScope: observabilityScope ) - // Update traits map here; before we make call to - // resolveDependencies below, which will check out - // the depenedencies we need. - guard !observabilityScope.errorsReported else { return currentManifests } @@ -606,14 +602,11 @@ extension Workspace { } } - try await self.updateEnabledTraitsMap() - // Create the constraints; filter unused dependencies. var computedConstraints = [PackageContainerConstraint]() computedConstraints += currentManifests.editedPackagesConstraints computedConstraints += try graphRoot.constraints(self.enabledTraitsMap) + constraints - manifestLoader // Perform dependency resolution. let resolver = try self.createResolver(resolvedPackages: resolvedPackagesStore.resolvedPackages, observabilityScope: observabilityScope) diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 4cf45e75ad8..72d2e6a67aa 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -624,27 +624,17 @@ extension Workspace { >] = { node in // optimization: preload manifest we know about in parallel // avoid loading dependencies that are trait-guarded here since this is redundant. - // load conditional traits, if any let dependenciesRequired = try node.item.manifest.dependenciesRequired( for: node.item.productFilter, node.item.enabledTraits ) - if node.item.identity.description.contains("apple-configuration") { - print("enabled traits for apple config: \(node.item.enabledTraits)") - } let dependenciesToLoad = dependenciesRequired.map(\.packageRef) .filter { !loadedManifests.keys.contains($0.identity) } - if node.item.identity.description.contains("apple-configuration") { - print("dependencies to load: \(dependenciesToLoad.map(\.identity.description))") - } try await prepopulateManagedDependencies(dependenciesToLoad) let dependenciesManifests = await self.loadManagedManifests( for: dependenciesToLoad, observabilityScope: observabilityScope ) - if node.item.identity.description.contains("apple-configuration") { - print("dependencies manifests loaded: \(dependenciesManifests.map(\.key.description))") - } dependenciesManifests.forEach { loadedManifests[$0.key] = $0.value } return try dependenciesRequired.compactMap { dependency in return try loadedManifests[dependency.identity].flatMap { manifest in @@ -749,54 +739,6 @@ extension Workspace { ) } - public func precomputeTraits( - _ topLevelManifests: [Manifest], - _ manifestMap: [PackageIdentity: Manifest] - ) throws -> [PackageIdentity: Set] { - var visited: Set = [] - - func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { - let parentTraits = self.enabledTraitsMap[parent.packageIdentity] - let requiredDependencies = try parent.dependenciesRequired(for: productFilter, parentTraits) - let guardedDependencies = parent.dependenciesTraitGuarded(withEnabledTraits: parentTraits) - - _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in - return try manifestMap[dependency.identity].flatMap({ manifest in - - let explicitlyEnabledTraits = dependency.traits?.filter { - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentTraits) - }.map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - let calculatedTraits = try manifest.enabledTraits( - using: enabledTraitsSet, - .init(parent) - ) - self.enabledTraitsMap[dependency.identity] = calculatedTraits - } - - let result = visited.insert(dependency.identity) - if result.inserted { - try dependencies(of: manifest, dependency.productFilter) - } - - return manifest - }) - }) - } - - for manifest in topLevelManifests { - // Track already-visited manifests to avoid cycles - let result = visited.insert(manifest.packageIdentity) - if result.inserted { - try dependencies(of: manifest) - } - } - - return self.enabledTraitsMap.dictionaryLiteral - } - /// Loads the given manifests, if it is present in the managed dependencies. /// @@ -973,9 +915,6 @@ extension Workspace { diagnostics: manifestLoadingDiagnostics, duration: duration ) - // update enabled traits map here - // Look for conditionally enabled traits in the manifest. -// let enabledTraits = manifest.enabledTraits(using: self.enabledTraitsMap[manifest.packageIdentity]) return manifest } diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift index 40043699eb5..5811d6da9c4 100644 --- a/Sources/Workspace/Workspace+Traits.swift +++ b/Sources/Workspace/Workspace+Traits.swift @@ -10,8 +10,57 @@ // //===----------------------------------------------------------------------===// +import class PackageModel.Manifest +import struct PackageModel.PackageIdentity +import enum PackageModel.ProductFilter + extension Workspace { - package func updateEnabledTraitsMap() { - + public func precomputeTraits( + _ topLevelManifests: [Manifest], + _ manifestMap: [PackageIdentity: Manifest] + ) throws -> [PackageIdentity: Set] { + var visited: Set = [] + + func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { + let parentTraits = self.enabledTraitsMap[parent.packageIdentity] + let requiredDependencies = try parent.dependenciesRequired(for: productFilter, parentTraits) + let guardedDependencies = parent.dependenciesTraitGuarded(withEnabledTraits: parentTraits) + + _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in + return try manifestMap[dependency.identity].flatMap({ manifest in + + let explicitlyEnabledTraits = dependency.traits?.filter { + guard let condition = $0.condition else { return true } + return condition.isSatisfied(by: parentTraits) + }.map(\.name) + + if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { + let calculatedTraits = try manifest.enabledTraits( + using: enabledTraitsSet, + .init(parent) + ) + self.enabledTraitsMap[dependency.identity] = calculatedTraits + } + + let result = visited.insert(dependency.identity) + if result.inserted { + try dependencies(of: manifest, dependency.productFilter) + } + + return manifest + }) + }) + } + + for manifest in topLevelManifests { + // Track already-visited manifests to avoid cycles + let result = visited.insert(manifest.packageIdentity) + if result.inserted { + try dependencies(of: manifest) + } + } + + return self.enabledTraitsMap.dictionaryLiteral } + } From adc81e1f235f4b96119b3a096a5fac4fb1add31a Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Wed, 22 Oct 2025 13:00:47 -0400 Subject: [PATCH 03/14] Cleanup --- Sources/Workspace/Workspace+Dependencies.swift | 1 - Sources/Workspace/Workspace+Manifests.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Sources/Workspace/Workspace+Dependencies.swift b/Sources/Workspace/Workspace+Dependencies.swift index 592329e88ab..90b2593a381 100644 --- a/Sources/Workspace/Workspace+Dependencies.swift +++ b/Sources/Workspace/Workspace+Dependencies.swift @@ -507,7 +507,6 @@ extension Workspace { // Ensure the cache path exists and validate that edited dependencies. self.createCacheDirectories(observabilityScope: observabilityScope) - // Load the root manifests and currently checked out manifests. let rootManifests = try await self.loadRootManifests( packages: root.packages, diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 72d2e6a67aa..d066afa5f5f 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -730,7 +730,6 @@ extension Workspace { dependencies.append((node.manifest, dependency, node.productFilter, fileSystem ?? self.fileSystem)) } - // dependency manifests returned here return DependencyManifests( root: root, dependencies: dependencies, From 80877adb89efd55674b8260eb57d99a51d59eea4 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 28 Oct 2025 03:04:35 -0400 Subject: [PATCH 04/14] Separate traits workspace tests + add EnabledTrait model * an EnabledTrait model has been created to store additional data about an enabled trait, namely the origins of its enablement. * EnabledTraits is a wrapper for a Set that handles the special edge cases when dealing with traits. * move traits-related tests in the WorkspaceTests to their own file --- Sources/PackageGraph/GraphLoadingNode.swift | 4 +- .../PackageGraph/ModulesGraph+Loading.swift | 8 +- Sources/PackageGraph/ModulesGraph.swift | 27 +- Sources/PackageGraph/PackageContainer.swift | 12 +- Sources/PackageGraph/PackageGraphRoot.swift | 14 +- .../PackageModel+Extensions.swift | 9 +- .../Resolution/DependencyResolutionNode.swift | 6 +- Sources/PackageLoading/PackageBuilder.swift | 6 +- Sources/PackageModel/EnabledTraitsMap.swift | 202 ++++- .../Manifest/Manifest+Traits.swift | 205 +++-- Sources/PackageModel/Manifest/Manifest.swift | 8 +- .../PackageDependencyDescription.swift | 13 + .../Manifest/TraitConfiguration.swift | 4 +- .../FileSystemPackageContainer.swift | 6 +- .../RegistryPackageContainer.swift | 6 +- .../SourceControlPackageContainer.swift | 10 +- .../ResolverPrecomputationProvider.swift | 6 +- .../Workspace/Workspace+Dependencies.swift | 17 +- Sources/Workspace/Workspace+Manifests.swift | 87 +- Sources/Workspace/Workspace+Traits.swift | 73 +- Sources/Workspace/Workspace.swift | 11 +- .../MockManifestLoader.swift | 2 +- .../MockPackageContainer.swift | 6 +- Tests/PackageGraphTests/PubGrubTests.swift | 6 +- Tests/PackageModelTests/ManifestTests.swift | 15 +- .../WorkspaceTests+Traits.swift | 792 ++++++++++++++++++ Tests/WorkspaceTests/WorkspaceTests.swift | 684 --------------- 27 files changed, 1343 insertions(+), 896 deletions(-) create mode 100644 Tests/WorkspaceTests/WorkspaceTests+Traits.swift diff --git a/Sources/PackageGraph/GraphLoadingNode.swift b/Sources/PackageGraph/GraphLoadingNode.swift index e0c4bb7a173..ec286e0f2dd 100644 --- a/Sources/PackageGraph/GraphLoadingNode.swift +++ b/Sources/PackageGraph/GraphLoadingNode.swift @@ -30,13 +30,13 @@ public struct GraphLoadingNode: Equatable, Hashable { public let productFilter: ProductFilter /// The enabled traits for this package. - package var enabledTraits: Set + package var enabledTraits: EnabledTraits public init( identity: PackageIdentity, manifest: Manifest, productFilter: ProductFilter, - enabledTraits: Set + enabledTraits: EnabledTraits ) throws { self.identity = identity self.manifest = manifest diff --git a/Sources/PackageGraph/ModulesGraph+Loading.swift b/Sources/PackageGraph/ModulesGraph+Loading.swift index 2ea14951129..1e06fef53c7 100644 --- a/Sources/PackageGraph/ModulesGraph+Loading.swift +++ b/Sources/PackageGraph/ModulesGraph+Loading.swift @@ -399,7 +399,7 @@ private func createResolvedPackages( return ResolvedPackageBuilder( package, productFilter: node.productFilter, - enabledTraits: node.enabledTraits /*?? []*/, + enabledTraits: node.enabledTraits, isAllowedToVendUnsafeProducts: isAllowedToVendUnsafeProducts, allowedToOverride: allowedToOverride, platformVersionProvider: platformVersionProvider @@ -1438,7 +1438,7 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { var products: [ResolvedProductBuilder] = [] /// The enabled traits of this package. - var enabledTraits: Set + var enabledTraits: EnabledTraits /// The dependencies of this package. var dependencies: [ResolvedPackageBuilder] = [] @@ -1462,7 +1462,7 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { init( _ package: Package, productFilter: ProductFilter, - enabledTraits: Set, + enabledTraits: EnabledTraits, isAllowedToVendUnsafeProducts: Bool, allowedToOverride: Bool, platformVersionProvider: PlatformVersionProvider @@ -1485,7 +1485,7 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { defaultLocalization: self.defaultLocalization, supportedPlatforms: self.supportedPlatforms, dependencies: self.dependencies.map(\.package.identity), - enabledTraits: self.enabledTraits, + enabledTraits: self.enabledTraits.names, modules: modules, products: products, registryMetadata: self.registryMetadata, diff --git a/Sources/PackageGraph/ModulesGraph.swift b/Sources/PackageGraph/ModulesGraph.swift index a5df0fb46e4..98b431f64ff 100644 --- a/Sources/PackageGraph/ModulesGraph.swift +++ b/Sources/PackageGraph/ModulesGraph.swift @@ -488,7 +488,7 @@ public func loadModulesGraph( _ enabledTraitsMap: EnabledTraitsMap, _ topLevelManifests: [Manifest], _ manifestMap: [PackageIdentity: Manifest] - ) throws -> [PackageIdentity: Set] { + ) throws -> [PackageIdentity: EnabledTraits] { var visited: Set = [] var enabledTraitsMap = enabledTraitsMap @@ -499,20 +499,23 @@ public func loadModulesGraph( _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in return try manifestMap[dependency.identity].flatMap({ manifest in - - let explicitlyEnabledTraits = dependency.traits?.filter { - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentTraits) - }.map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - let calculatedTraits = try manifest.enabledTraits( - using: enabledTraitsSet, - .init(parent) + let explicitlyEnabledTraitsSet = dependency.traits?.filter({ $0.isEnabled(by: parentTraits) }).map(\.name) + if let explicitlyEnabledTraitsSet { + let explicitlyEnabledTraits = EnabledTraits( + explicitlyEnabledTraitsSet, + setBy: .package(.init(identity: parent.packageIdentity, name: parent.displayName)) ) - enabledTraitsMap[dependency.identity] = calculatedTraits + enabledTraitsMap[dependency.identity] = try manifest.enabledTraits(using: explicitlyEnabledTraits) } +// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { +// let calculatedTraits = try manifest.enabledTraits( +// using: enabledTraitsSet +//// .init(parent) +// ) +// enabledTraitsMap[dependency.identity] = calculatedTraits +// } + let result = visited.insert(dependency.identity) if result.inserted { try dependencies(of: manifest, dependency.productFilter) diff --git a/Sources/PackageGraph/PackageContainer.swift b/Sources/PackageGraph/PackageContainer.swift index 031799a22da..f1029ad3d5f 100644 --- a/Sources/PackageGraph/PackageContainer.swift +++ b/Sources/PackageGraph/PackageContainer.swift @@ -75,7 +75,7 @@ public protocol PackageContainer { /// - Precondition: `versions.contains(version)` /// - Throws: If the version could not be resolved; this will abort /// dependency resolution completely. - func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set) async throws -> [PackageContainerConstraint] + func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits) async throws -> [PackageContainerConstraint] /// Fetch the declared dependencies for a particular revision. /// @@ -84,12 +84,12 @@ public protocol PackageContainer { /// /// - Throws: If the revision could not be resolved; this will abort /// dependency resolution completely. - func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set) async throws -> [PackageContainerConstraint] + func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits) async throws -> [PackageContainerConstraint] /// Fetch the dependencies of an unversioned package container. /// /// NOTE: This method should not be called on a versioned container. - func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set) async throws -> [PackageContainerConstraint] + func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits) async throws -> [PackageContainerConstraint] /// Get the updated identifier at a bound version. /// @@ -150,11 +150,11 @@ public struct PackageContainerConstraint: Equatable, Hashable { public let products: ProductFilter /// The traits that have been enabled for the package. - public let enabledTraits: Set + public let enabledTraits: EnabledTraits /// Create a constraint requiring the given `container` satisfying the /// `requirement`. - public init(package: PackageReference, requirement: PackageRequirement, products: ProductFilter, enabledTraits: Set = ["default"]) { + public init(package: PackageReference, requirement: PackageRequirement, products: ProductFilter, enabledTraits: EnabledTraits = ["default"]) { self.package = package self.requirement = requirement self.products = products @@ -163,7 +163,7 @@ public struct PackageContainerConstraint: Equatable, Hashable { /// Create a constraint requiring the given `container` satisfying the /// `versionRequirement`. - public init(package: PackageReference, versionRequirement: VersionSetSpecifier, products: ProductFilter, enabledTraits: Set = ["default"]) { + public init(package: PackageReference, versionRequirement: VersionSetSpecifier, products: ProductFilter, enabledTraits: EnabledTraits = ["default"]) { self.init(package: package, requirement: .versionSet(versionRequirement), products: products, enabledTraits: enabledTraits) } diff --git a/Sources/PackageGraph/PackageGraphRoot.swift b/Sources/PackageGraph/PackageGraphRoot.swift index 2bdcff664a0..170c76e894f 100644 --- a/Sources/PackageGraph/PackageGraphRoot.swift +++ b/Sources/PackageGraph/PackageGraphRoot.swift @@ -115,7 +115,7 @@ public struct PackageGraphRoot { // If not, then we can omit this dependency if pruning unused dependencies // is enabled. return manifests.values.reduce(false) { result, manifest in - let enabledTraits: Set = enabledTraitsMap[manifest.packageIdentity] + let enabledTraits = enabledTraitsMap[manifest.packageIdentity] if let isUsed = try? manifest.isPackageDependencyUsed(dep, enabledTraits: enabledTraits) { return result || isUsed } @@ -128,7 +128,7 @@ public struct PackageGraphRoot { // FIXME: `dependenciesRequired` modifies manifests and prevents conversion of `Manifest` to a value type let deps = try? manifests.values.lazy .map({ manifest -> [PackageDependency] in - let enabledTraits: Set = enabledTraitsMap[manifest.packageIdentity] + let enabledTraits = enabledTraitsMap[manifest.packageIdentity] return try manifest.dependenciesRequired(for: .everything, enabledTraits) }) .flatMap({ $0 }) @@ -145,7 +145,7 @@ public struct PackageGraphRoot { /// Returns the constraints imposed by root manifests + dependencies. public func constraints(_ enabledTraitsMap: EnabledTraitsMap) throws -> [PackageContainerConstraint] { - var rootEnabledTraits: Set = [] + var rootEnabledTraits: Set = [] let constraints = self.packages.map { (identity, package) in let enabledTraits = enabledTraitsMap[identity] rootEnabledTraits.formUnion(enabledTraits) @@ -161,11 +161,13 @@ public struct PackageGraphRoot { .map { dep in let enabledTraits = dep.traits?.filter { guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: rootEnabledTraits) - }.map(\.name) + return condition.isSatisfied(by: rootEnabledTraits.names) + // TODO bp modify this. + }.map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: "root", name: "root"))) }) + // TODO bp enabled traits map must flatten default traits before this var enabledTraitsSet = enabledTraitsMap[dep.identity] - enabledTraitsSet.formUnion(enabledTraits.flatMap({ Set($0) }) ?? []) + enabledTraitsSet.formUnion(EnabledTraits(enabledTraits ?? [])) // TODO bp modify this. return PackageContainerConstraint( package: dep.packageRef, diff --git a/Sources/PackageGraph/PackageModel+Extensions.swift b/Sources/PackageGraph/PackageModel+Extensions.swift index fbb9888039b..e75daa9df9e 100644 --- a/Sources/PackageGraph/PackageModel+Extensions.swift +++ b/Sources/PackageGraph/PackageModel+Extensions.swift @@ -35,14 +35,15 @@ extension PackageDependency { extension Manifest { /// Constructs constraints of the dependencies in the raw package. - public func dependencyConstraints(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func dependencyConstraints(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { return try self.dependenciesRequired(for: productFilter, enabledTraits).map({ let explicitlyEnabledTraits = $0.traits?.filter { guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: enabledTraits) - }.map(\.name) + return condition.isSatisfied(by: enabledTraits.names) + }.map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: self.packageIdentity, name: self.displayName))) }) - let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) ?? ["default"] + // TODO bp enabledTraitsMap must be propagated here? + let enabledTraitsSet = EnabledTraits(explicitlyEnabledTraits ?? []) //explicitlyEnabledTraits.flatMap({ Set($0) }) ?? ["default"] return PackageContainerConstraint( package: $0.packageRef, diff --git a/Sources/PackageGraph/Resolution/DependencyResolutionNode.swift b/Sources/PackageGraph/Resolution/DependencyResolutionNode.swift index 11693a109c7..5c71a8abd30 100644 --- a/Sources/PackageGraph/Resolution/DependencyResolutionNode.swift +++ b/Sources/PackageGraph/Resolution/DependencyResolutionNode.swift @@ -44,7 +44,7 @@ public enum DependencyResolutionNode { /// Since a non‐existent product ends up with only its implicit dependency on its own package, /// only whichever package contains the product will end up adding additional constraints. /// See `ProductFilter` and `Manifest.register(...)`. - case product(String, package: PackageReference, enabledTraits: Set = ["default"]) + case product(String, package: PackageReference, enabledTraits: EnabledTraits = ["default"]) /// A root node. /// @@ -58,7 +58,7 @@ public enum DependencyResolutionNode { /// It is a warning condition, and builds do not actually need these dependencies. /// However, forcing the graph to resolve and fetch them anyway allows the diagnostics passes access /// to the information needed in order to provide actionable suggestions to help the user stitch up the dependency declarations properly. - case root(package: PackageReference, enabledTraits: Set = ["default"]) + case root(package: PackageReference, enabledTraits: EnabledTraits = ["default"]) /// The package. public var package: PackageReference { @@ -91,7 +91,7 @@ public enum DependencyResolutionNode { } /// Returns the enabled traits for this node's manifest. - public var enabledTraits: Set { + public var enabledTraits: EnabledTraits { switch self { case .root(_, let enabledTraits), .product(_, _, let enabledTraits): return enabledTraits diff --git a/Sources/PackageLoading/PackageBuilder.swift b/Sources/PackageLoading/PackageBuilder.swift index 08535c82807..77169042aa0 100644 --- a/Sources/PackageLoading/PackageBuilder.swift +++ b/Sources/PackageLoading/PackageBuilder.swift @@ -388,7 +388,7 @@ public final class PackageBuilder { private var swiftVersionCache: SwiftLanguageVersion? = nil /// The enabled traits of this package. - private let enabledTraits: Set + private let enabledTraits: EnabledTraits /// Create a builder for the given manifest and package `path`. /// @@ -414,7 +414,7 @@ public final class PackageBuilder { createREPLProduct: Bool = false, fileSystem: FileSystem, observabilityScope: ObservabilityScope, - enabledTraits: Set + enabledTraits: EnabledTraits ) { self.identity = identity self.manifest = manifest @@ -1151,7 +1151,7 @@ public final class PackageBuilder { // Process each setting. for setting in target.settings { - if let traits = setting.condition?.traits, traits.intersection(self.enabledTraits).isEmpty { + if let traits = setting.condition?.traits, traits.intersection(self.enabledTraits.names).isEmpty { // The setting is currently not enabled so we should skip it continue } diff --git a/Sources/PackageModel/EnabledTraitsMap.swift b/Sources/PackageModel/EnabledTraitsMap.swift index a7338095511..b99564c5558 100644 --- a/Sources/PackageModel/EnabledTraitsMap.swift +++ b/Sources/PackageModel/EnabledTraitsMap.swift @@ -10,12 +10,14 @@ // //===----------------------------------------------------------------------===// +import Basics + /// A wrapper for a dictionary that stores the transitively enabled traits for each package. public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { public typealias Key = PackageIdentity - public typealias Value = Set + public typealias Value = EnabledTraits - var storage: [PackageIdentity: Set] = [:] + var storage: [PackageIdentity: EnabledTraits] = [:] public init() { } @@ -29,13 +31,14 @@ public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { self.storage = dictionary } - public subscript(key: PackageIdentity) -> Set { + public subscript(key: PackageIdentity/*, setBy: EnabledTrait.Origin = .traitConfiguration*/) -> EnabledTraits { get { storage[key] ?? ["default"] } set { // Omit adding "default" explicitly, since the map returns "default" // if there is no explicit traits declared. This will allow us to check // for nil entries in the stored dictionary, which tells us whether // traits have been explicitly declared. + print("adding \(newValue) traits to \(key.description)") guard newValue != ["default"] else { return } if storage[key] == nil { storage[key] = newValue @@ -45,11 +48,200 @@ public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { } } - public subscript(explicitlyEnabledTraitsFor key: PackageIdentity) -> Set? { + public subscript(explicitlyEnabledTraitsFor key: PackageIdentity) -> EnabledTraits? { get { storage[key] } } - public var dictionaryLiteral: [PackageIdentity: Set] { + public var dictionaryLiteral: [PackageIdentity: EnabledTraits] { return storage } } + +public struct EnabledTrait: Hashable, CustomStringConvertible, ExpressibleByStringLiteral, Comparable { + public let name: String + public var setBy: Set = [] + + public enum Origin: Hashable, CustomStringConvertible { + case `default` + case traitConfiguration + case package(Manifest.PackageIdentifier) + case trait(String) + + public var description: String { + switch self { + case .default: + "default" + case .traitConfiguration: + "custom trait configuration." + case .package(let parent): + parent.description + case .trait(let trait): + trait + } + } + + public var parentPackage: Manifest.PackageIdentifier? { + switch self { + case .package(let id): + return id + case .traitConfiguration, .trait, .default: + return nil + } + } + } + + public init(name: String, setBy: Origin) { + self.name = name + self.setBy = [setBy] + } + + public var parentPackages: [Manifest.PackageIdentifier] { + setBy.compactMap(\.parentPackage) + } + +// public mutating func formUnion(_ otherOrigin: Set) { +// self.setBy.formUnion(otherOrigin) +// } + + public func union(_ otherTrait: EnabledTrait) -> EnabledTrait? { + guard self.name == otherTrait.name else { + return nil + } + + var updatedTrait = self + updatedTrait.setBy = setBy.union(otherTrait.setBy) + return updatedTrait + } + + // Static helper method to create Set from a Collection of String. + public static func createSet( + from traits: C, + enabledBy origin: EnabledTrait.Origin + ) -> EnabledTraits where C.Element == String { + .init(Set(traits.map({ EnabledTrait(name: $0, setBy: origin)}))) + } + + // MARK: - CustomStringConvertible + public var description: String { + name + } + + // MARK: - ExpressibleByStringLiteral + public init(stringLiteral value: String) { + self.name = value + } + + // MARK: - Equatable + + // When comparing two `EnabledTraits`, if the names are the same then + // we know that these two objects are referring to the same trait of a package. + // In this case, the two objects should be combined into one. + public static func ==(lhs: EnabledTrait, rhs: EnabledTrait) -> Bool { + lhs.name == rhs.name + } + + public static func ==(lhs: EnabledTrait, rhs: String) -> Bool { + lhs.name == rhs + } + + public static func ==(lhs: String, rhs: EnabledTrait) -> Bool { + lhs == rhs.name + } + + // MARK: - Comparable + + public static func <(lhs: EnabledTrait, rhs: EnabledTrait) -> Bool { + return lhs.name < rhs.name + } +} + +// A wrapper struct for a set of `EnabledTrait` to handle special cases. +public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { + public typealias Element = EnabledTrait + public typealias Index = Set.Index + + private var _traits: Set = [] + + public init(arrayLiteral elements: EnabledTrait...) { + for element in elements { + _traits.insert(element) + } + } + + public init(_ traits: C, setBy origin: EnabledTrait.Origin) where C.Element == String { + let traits = Set(traits.map({ EnabledTrait(name: $0, setBy: origin) })) + self.init(traits) + } + + public init(_ traits: C) where C.Element == EnabledTrait { + self._traits = Set(traits) + } + + public var startIndex: Index { + return _traits.startIndex + } + + public var endIndex: Index { + return _traits.endIndex + } + + public func index(after i: Index) -> Index { + return _traits.index(after: i) + } + + public subscript(position: Index) -> Element { + return _traits[position] + } + + public mutating func formUnion(_ other: EnabledTraits) { + _traits = Set( + _traits.compactMap { trait in + if let otherTrait = other.first(where: { $0 == trait }) { + return trait.union(otherTrait) + } else { + return trait + } + } + ) + } + + public func flatMap(_ transform: (Self.Element) throws -> Self) rethrows -> Self { + let transformedTraits = try _traits.flatMap(transform) + return EnabledTraits(transformedTraits) + } + + public func union(_ other: EnabledTraits) -> EnabledTraits { + print("enabled traits in self: \(_traits)") + print("to union with: \(other)") + let unionedTraits = _traits.union(other) + print("after union: \(unionedTraits)") + return EnabledTraits(unionedTraits) + } + + public mutating func remove(_ member: Element) -> Element? { + return _traits.remove(member) + } + + public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) { + return _traits.insert(newMember) + } + + public func contains(_ member: Element) -> Bool { + return _traits.contains(member) + } +} + +extension Collection where Element == EnabledTrait { + public func contains(_ trait: String) -> Bool { + self.map(\.name).contains(trait) + } + + public var names: Set { + Set(self.map(\.name)) + } + + public func joined(separator: String = "") -> String { + names.joined(separator: separator) + } +} + diff --git a/Sources/PackageModel/Manifest/Manifest+Traits.swift b/Sources/PackageModel/Manifest/Manifest+Traits.swift index a5535e828d4..ac5a449c19a 100644 --- a/Sources/PackageModel/Manifest/Manifest+Traits.swift +++ b/Sources/PackageModel/Manifest/Manifest+Traits.swift @@ -17,10 +17,15 @@ import Foundation /// Validator methods that check the correctness of traits and their support as defined in the manifest. extension Manifest { + /// Struct that contains information about a package's identity, as well as its name. public struct PackageIdentifier: Hashable, CustomStringConvertible { public var identity: String public var name: String? + public init(identity: PackageIdentity, name: String? = nil) { + self.init(identity: identity.description, name: name) + } + public init(identity: String, name: String? = nil) { self.identity = identity self.name = name @@ -51,7 +56,7 @@ extension Manifest { if !supportsTraits { throw TraitError.invalidTrait( package: .init(self), - trait: trait.name, + trait: .init(stringLiteral: trait.name), availableTraits: traits.map({ $0.name }) ) } @@ -59,11 +64,11 @@ extension Manifest { return } - try self.validateTrait(trait.name) + try self.validateTrait(EnabledTrait(stringLiteral: trait.name)) } /// Validates a trait by checking that it is defined in the manifest; if not, an error is thrown. - private func validateTrait(_ trait: String, parentPackage: PackageIdentifier? = nil) throws { + private func validateTrait(_ trait: EnabledTrait) throws { guard trait != "default" else { if !supportsTraits { throw TraitError.invalidTrait( @@ -77,12 +82,11 @@ extension Manifest { } // Check if the passed trait is a valid trait. - if self.traits.first(where: { $0.name == trait }) == nil { + if self.traits.first(where: { $0.name == trait.name }) == nil { throw TraitError.invalidTrait( package: .init(self), trait: trait, - availableTraits: self.traits.map({ $0.name }), - parent: parentPackage + availableTraits: self.traits.map({ $0.name }) ) } } @@ -91,13 +95,13 @@ extension Manifest { /// set of enabled traits and whether the manifest defines these traits (or if it defines any traits at all), then an /// error indicating the issue will be thrown. private func validateEnabledTraits( - _ explicitlyEnabledTraits: Set, - _ parentPackage: PackageIdentifier? = nil + _ explicitlyEnabledTraits: EnabledTraits, +// _ parentPackage: PackageIdentifier? = nil ) throws { guard supportsTraits else { if explicitlyEnabledTraits != ["default"] { throw TraitError.traitsNotSupported( - parent: parentPackage, +// parent: parentPackage, package: .init(self), explicitlyEnabledTraits: explicitlyEnabledTraits.map({ $0 }) ) @@ -110,7 +114,7 @@ extension Manifest { // Validate each trait to assure it's defined in the current package. for trait in enabledTraits { - try validateTrait(trait, parentPackage: parentPackage) + try validateTrait(trait) } let areDefaultsEnabled = enabledTraits.contains("default") @@ -120,7 +124,7 @@ extension Manifest { // We throw an error when default traits are disabled for a package without any traits // This allows packages to initially move new API behind traits once. throw TraitError.traitsNotSupported( - parent: parentPackage, +// parent: parentPackage, package: .init(self), explicitlyEnabledTraits: enabledTraits.map({ $0 }) ) @@ -132,15 +136,13 @@ extension Manifest { switch traitConfiguration { case .disableAllTraits: throw TraitError.traitsNotSupported( - parent: nil, package: .init(self), explicitlyEnabledTraits: [] ) case .enabledTraits(let traits): throw TraitError.traitsNotSupported( - parent: nil, package: .init(self), - explicitlyEnabledTraits: traits.map({ $0 }) + explicitlyEnabledTraits: traits.map({ .init(stringLiteral: $0) }) ) case .enableAllTraits, .default: return @@ -150,7 +152,7 @@ extension Manifest { // Get the enabled traits; if the trait configuration's `.enabledTraits` returns nil, // we know that it's the `.enableAllTraits` case, since the config does not store // all the defined traits of the manifest itself. - let enabledTraits = traitConfiguration.enabledTraits ?? Set(self.traits.map({ $0.name })) + let enabledTraits: EnabledTraits = traitConfiguration.enabledTraits ?? EnabledTrait.createSet(from: self.traits.map(\.name), enabledBy: .traitConfiguration) try validateEnabledTraits(enabledTraits) } @@ -162,10 +164,10 @@ extension Manifest { /// Helper methods to calculate states of the manifest and its dependencies when given a set of enabled traits. extension Manifest { /// The default traits as defined in this package as the root. - public var defaultTraits: Set? { + public var defaultTraits: Set? { // First, guard against whether this package actually has traits. guard self.supportsTraits else { return nil } - return self.traits.filter(\.isDefault) + return Set(self.traits.filter(\.isDefault).flatMap(\.enabledTraits)) } /// A map of trait names to the trait description. @@ -177,7 +179,7 @@ extension Manifest { /// Calculates the set of all transitive traits that are enabled for this manifest using the passed trait configuration. /// Since a trait configuration is only used for root packages, this method is intended for use with root packages only. - public func enabledTraits(using traitConfiguration: TraitConfiguration) throws -> Set { + public func enabledTraits(using traitConfiguration: TraitConfiguration) throws -> EnabledTraits { // If this manifest does not support traits, but the passed configuration either // disables default traits or enables non-default traits (i.e. traits that would // not exist for this manifest) then we must throw an error. @@ -186,22 +188,22 @@ extension Manifest { return ["default"] } - var enabledTraits: Set = [] + var enabledTraits: EnabledTraits = [] switch traitConfiguration { case .enableAllTraits: - enabledTraits = Set(traits.map(\.name)) + enabledTraits = EnabledTraits(traits.map(\.name), setBy: .traitConfiguration) case .default: - if let defaultTraits = defaultTraits?.map(\.name) { - enabledTraits = Set(defaultTraits) + if let defaultTraits = defaultTraits { + enabledTraits = EnabledTraits(defaultTraits, setBy: .default) } case .disableAllTraits: return [] case .enabledTraits(let explicitlyEnabledTraits): - enabledTraits = explicitlyEnabledTraits + enabledTraits = EnabledTraits(explicitlyEnabledTraits, setBy: .traitConfiguration) } - if let allEnabledTraits = try? self.enabledTraits(using: enabledTraits, nil) { + if let allEnabledTraits = try? self.enabledTraits(using: enabledTraits) { enabledTraits = allEnabledTraits } @@ -211,18 +213,18 @@ extension Manifest { /// Calculates the set of all transitive traits that are enabled for this manifest using the passed set of /// explicitly enabled traits, and the parent package that defines the enabled traits for this package. /// This method is intended for use with non-root packages. - public func enabledTraits(using explicitlyEnabledTraits: Set = ["default"], _ parentPackage: PackageIdentifier?) throws -> Set { + public func enabledTraits(using explicitlyEnabledTraits: EnabledTraits = ["default"]) throws -> EnabledTraits { // If this manifest does not support traits, but the passed configuration either // disables default traits or enables non-default traits (i.e. traits that would // not exist for this manifest) then we must throw an error. - try validateEnabledTraits(explicitlyEnabledTraits, parentPackage) + try validateEnabledTraits(explicitlyEnabledTraits) guard supportsTraits else { return ["default"] } - var enabledTraits: Set = [] + var enabledTraits: EnabledTraits = [] - if let allEnabledTraits = try? calculateAllEnabledTraits(explictlyEnabledTraits: explicitlyEnabledTraits, parentPackage) { + if let allEnabledTraits = try? calculateAllEnabledTraits(explictlyEnabledTraits: explicitlyEnabledTraits) { enabledTraits = allEnabledTraits } @@ -230,7 +232,7 @@ extension Manifest { } /// Determines if a trait is enabled with a given set of enabled traits. - public func isTraitEnabled(_ trait: TraitDescription, _ enabledTraits: Set) throws -> Bool { + public func isTraitEnabled(_ trait: TraitDescription, _ enabledTraits: EnabledTraits) throws -> Bool { // First, check that the queried trait is valid. try validateTrait(trait) // Then, check that the list of enabled traits is valid. @@ -258,7 +260,7 @@ extension Manifest { // If manifest does not define default traits, then throw an invalid trait error. throw TraitError.invalidTrait( package: .init(self), - trait: trait.name, + trait: EnabledTrait(stringLiteral: trait.name), availableTraits: self.traits.map(\.name) ) } @@ -291,7 +293,7 @@ extension Manifest { // If manifest does not define default traits, then throw an invalid trait error. throw TraitError.invalidTrait( package: .init(self), - trait: trait.name, + trait: .init(stringLiteral: trait.name), availableTraits: self.traits.map(\.name) ) } @@ -304,38 +306,45 @@ extension Manifest { /// Calculates and returns a set of all enabled traits, beginning with a set of explicitly enabled traits (which can either be the default traits of a manifest, or a configuration of enabled traits determined from a user-generated trait configuration) and determines which traits are transitively enabled. private func calculateAllEnabledTraits( - explictlyEnabledTraits: Set, - _ parentPackage: PackageIdentifier? = nil - ) throws -> Set { - try validateEnabledTraits(explictlyEnabledTraits, parentPackage) + explictlyEnabledTraits: EnabledTraits, +// _ parentPackage: PackageIdentifier? = nil + ) throws -> EnabledTraits { + try validateEnabledTraits(explictlyEnabledTraits) // This the point where we flatten the enabled traits and resolve the recursive traits var enabledTraits = explictlyEnabledTraits - let areDefaultsEnabled = enabledTraits.remove("default") != nil + let areDefaultsEnabled = enabledTraits.remove("default") != nil // TODO bp check if this remove is ok // We have to enable all default traits if no traits are enabled or the defaults are explicitly enabled if explictlyEnabledTraits == ["default"] || areDefaultsEnabled { if let defaultTraits { - enabledTraits.formUnion(defaultTraits.flatMap(\.enabledTraits)) + let transitiveDefaultTraits = EnabledTrait.createSet( + from: defaultTraits, + enabledBy: .trait("default") + ) + enabledTraits.formUnion(transitiveDefaultTraits) } } + // Initialize before loop to avoid recalculations of computed property + let traitsMap = traitsMap + // Iteratively flatten transitively enabled traits; stop when all transitive traits have been found. while true { - let transitivelyEnabledTraits = try Set( - // 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. - enabledTraits - .flatMap { trait in - guard let traitDescription = traitsMap[trait] else { - throw TraitError.invalidTrait( - package: .init(self), - trait: trait, - parent: parentPackage - ) - } - return traitDescription.enabledTraits - } - ) + // 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. + let transitivelyEnabledTraits = try enabledTraits.flatMap { trait in + guard let traitDescription = traitsMap[trait.name] else { + throw TraitError.invalidTrait( + package: .init(self), + trait: trait + ) + } + return EnabledTrait.createSet( + from: traitDescription.enabledTraits, + enabledBy: .trait(traitDescription.name) + ) + } + let appendedList = enabledTraits.union(transitivelyEnabledTraits) if appendedList.count == enabledTraits.count { @@ -349,7 +358,7 @@ extension Manifest { } /// Computes the dependencies that are in use per target in this manifest. - public func usedTargetDependencies(withTraits enabledTraits: Set) throws -> [String: Set] { + public func usedTargetDependencies(withTraits enabledTraits: EnabledTraits) throws -> [String: Set] { try self.targets.reduce(into: [String: Set]()) { depMap, target in let nonTraitDeps = target.dependencies.filter { $0.condition?.traits?.isEmpty ?? true @@ -364,7 +373,8 @@ extension Manifest { } // For each trait that is a condition on this target dependency, assure that // each one is enabled in the manifest. - return try traits.allSatisfy({ try isTraitEnabled(.init(stringLiteral: $0), enabledTraits) }) +// return try traits.allSatisfy({ try isTraitEnabled(.init(stringLiteral: $0), enabledTraits) }) + return !traits.intersection(enabledTraits.names).isEmpty } let deps = nonTraitDeps + traitGuardedDeps @@ -373,7 +383,7 @@ extension Manifest { } /// Computes the set of package dependencies that are used by targets of this manifest. - public func usedDependencies(withTraits enabledTraits: Set) throws -> (knownPackage: Set, unknownPackage: Set) { + public func usedDependencies(withTraits enabledTraits: EnabledTraits) throws -> (knownPackage: Set, unknownPackage: Set) { let deps = try self.usedTargetDependencies(withTraits: enabledTraits) .values .flatMap { $0 } @@ -437,7 +447,7 @@ extension Manifest { public func isTargetDependencyEnabled( target: String, _ dependency: TargetDescription.Dependency, - enabledTraits: Set, + enabledTraits: EnabledTraits, ) throws -> Bool { guard self.supportsTraits else { return true } guard let target = self.targetMap[target] else { return false } @@ -460,7 +470,8 @@ extension Manifest { return traitsToEnable.isEmpty || isEnabled } /// Determines whether a given package dependency is used by this manifest given a set of enabled traits. - public func isPackageDependencyUsed(_ dependency: PackageDependency, enabledTraits: Set) throws -> Bool { + public func isPackageDependencyUsed(_ dependency: PackageDependency, enabledTraits: EnabledTraits) throws -> Bool { + let isTraitGuarded = try isTraitGuarded(dependency, enabledTraits: enabledTraits) if self.pruneDependencies { let usedDependencies = try self.usedDependencies(withTraits: enabledTraits) let foundKnownPackage = usedDependencies.knownPackage.contains(where: { @@ -471,24 +482,49 @@ extension Manifest { // tentatively marking the package dependency as used. to be resolved later on. return foundKnownPackage || (!foundKnownPackage && !usedDependencies.unknownPackage.isEmpty) } else { + return !isTraitGuarded // alternate path to compute trait-guarded package dependencies if the prune deps feature is not enabled - try validateEnabledTraits(enabledTraits) +// try validateEnabledTraits(enabledTraits) +// +// let targetDependenciesForPackageDependency = self.targets.flatMap({ $0.dependencies }) +// .filter({ +// $0.package?.caseInsensitiveCompare(dependency.identity.description) == .orderedSame +// }) +// +// // if target deps is empty, default to returning true here. +// let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({ +// let isGuarded = $0.intersection(enabledTraits).isEmpty +// return isGuarded +// }) +// +// let isUsedWithoutTraitGuarding = !targetDependenciesForPackageDependency.filter({ $0.condition?.traits == nil }).isEmpty +// +// return isUsedWithoutTraitGuarding || !isTraitGuarded + } + } - let targetDependenciesForPackageDependency = self.targets.flatMap({ $0.dependencies }) - .filter({ - $0.package?.caseInsensitiveCompare(dependency.identity.description) == .orderedSame - }) + private func isTraitGuarded(_ dependency: PackageDependency, enabledTraits: EnabledTraits) throws -> Bool { + try validateEnabledTraits(enabledTraits) - // if target deps is empty, default to returning true here. - let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({ - let isGuarded = $0.intersection(enabledTraits).isEmpty - return isGuarded - }) + let targetDependenciesForPackageDependency = self.targets.flatMap({ $0.dependencies }) + .filter({ + $0.package?.caseInsensitiveCompare(dependency.identity.description) == .orderedSame + }) - let isUsedWithoutTraitGuarding = !targetDependenciesForPackageDependency.filter({ $0.condition?.traits == nil }).isEmpty + let enabledTraitNames = enabledTraits.names - return isUsedWithoutTraitGuarding || !isTraitGuarded - } + // Determine whether the current set of enabled traits still gate the package dependency + // across targets. + let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({ + let isGuarded = $0.intersection(enabledTraitNames).isEmpty + return isGuarded + }) + + // Since we only omit a package dependency that is only guarded by traits, determine + // whether this dependency is used elsewhere without traits. + let isUsedWithoutTraitGuarding = !targetDependenciesForPackageDependency.filter({ $0.condition?.traits == nil }).isEmpty + + return !isUsedWithoutTraitGuarding && isTraitGuarded } } @@ -498,28 +534,29 @@ public enum TraitError: Swift.Error { /// Indicates that an invalid trait was enabled. case invalidTrait( package: Manifest.PackageIdentifier, - trait: String, - availableTraits: [String] = [], - parent: Manifest.PackageIdentifier? = nil + trait: EnabledTrait, + availableTraits: [String] = [] ) /// Indicates that the manifest does not support traits, yet a method was called with a configuration of enabled /// traits. case traitsNotSupported( - parent: Manifest.PackageIdentifier? = nil, package: Manifest.PackageIdentifier, - explicitlyEnabledTraits: [String] + explicitlyEnabledTraits: [EnabledTrait] ) } extension TraitError: CustomStringConvertible { public var description: String { switch self { - case .invalidTrait(let package, let trait, var availableTraits, let parentPackage): + case .invalidTrait(let package, let trait, var availableTraits): availableTraits = availableTraits.sorted() var errorMsg = "Trait '\(trait)'" - if let parentPackage { - errorMsg += " enabled by parent package \(parentPackage)" + let parentPackages = Set(trait.parentPackages) + let parent: Manifest.PackageIdentifier? = parentPackages.count == 1 ? parentPackages.first : nil + + if let parent { + errorMsg += " enabled by parent package \(parent)" } errorMsg += " is not declared by package \(package)." if availableTraits.isEmpty { @@ -529,12 +566,14 @@ extension TraitError: CustomStringConvertible { " The available traits declared by this package are: \(availableTraits.joined(separator: ", "))." } return errorMsg - case .traitsNotSupported(let parentPackage, let package, var explicitlyEnabledTraits): + case .traitsNotSupported(let package, var explicitlyEnabledTraits): explicitlyEnabledTraits = explicitlyEnabledTraits.sorted() + let parentPackages = Set(explicitlyEnabledTraits.compactMap(\.parentPackages).flatMap({ $0 })) + let parent: Manifest.PackageIdentifier? = parentPackages.count == 1 ? parentPackages.first : nil if explicitlyEnabledTraits.isEmpty { - if let parentPackage { + if let parent { return """ - Disabled default traits by package \(parentPackage) on package \(package) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. + Disabled default traits by package \(parent) on package \(package) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. """ } else { return """ @@ -542,9 +581,9 @@ extension TraitError: CustomStringConvertible { """ } } else { - if let parentPackage { + if let parent { return """ - Package \(parentPackage) enables traits [\(explicitlyEnabledTraits.joined(separator: ", "))] on package \(package) that declares no traits. + Package \(parent) enables traits [\(explicitlyEnabledTraits.joined(separator: ", "))] on package \(package) that declares no traits. """ } else { return """ diff --git a/Sources/PackageModel/Manifest/Manifest.swift b/Sources/PackageModel/Manifest/Manifest.swift index 396961e11d7..5d4220e314f 100644 --- a/Sources/PackageModel/Manifest/Manifest.swift +++ b/Sources/PackageModel/Manifest/Manifest.swift @@ -199,13 +199,13 @@ public final class Manifest: Sendable { /// /// If we set the `enabledTraits` to be `["Trait1"]`, then the list of dependencies guarded by traits would be `[]`. /// Otherwise, if `enabledTraits` were `nil`, then the dependencies guarded by traits would be `["Bar"]`. - public func dependenciesTraitGuarded(withEnabledTraits enabledTraits: Set) -> [PackageDependency] { + public func dependenciesTraitGuarded(withEnabledTraits enabledTraits: EnabledTraits) -> [PackageDependency] { guard supportsTraits else { return [] } let traitGuardedDeps = self.traitGuardedTargetDependencies(lowercasedKeys: true) - let explicitlyEnabledTraits = try? self.enabledTraits(using: enabledTraits, nil) + let explicitlyEnabledTraits = try? self.enabledTraits(using: enabledTraits) guard self.toolsVersion >= .v5_2 && !self.packageKind.isRoot else { let deps = self.dependencies.filter { @@ -249,7 +249,7 @@ public final class Manifest: Sendable { continue } - if guardingTraits.intersection(enabledTraits) != guardingTraits + if guardingTraits.intersection(enabledTraits.names) != guardingTraits { guardedDependencies.insert(dependency.identity) } @@ -266,7 +266,7 @@ public final class Manifest: Sendable { /// Returns the package dependencies required for a particular products filter and trait configuration. public func dependenciesRequired( for productFilter: ProductFilter, - _ enabledTraits: Set = ["default"] + _ enabledTraits: EnabledTraits = ["default"] ) throws -> [PackageDependency] { #if ENABLE_TARGET_BASED_DEPENDENCY_RESOLUTION // If we have already calculated it, returned the cached value. diff --git a/Sources/PackageModel/Manifest/PackageDependencyDescription.swift b/Sources/PackageModel/Manifest/PackageDependencyDescription.swift index b4bfffcaa96..d979d90e79b 100644 --- a/Sources/PackageModel/Manifest/PackageDependencyDescription.swift +++ b/Sources/PackageModel/Manifest/PackageDependencyDescription.swift @@ -31,6 +31,7 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } public func isSatisfied(by enabledTraits: Set) -> Bool { + // If there are no traits in this condition, default to true. guard let traits else { return true } return !traits.intersection(enabledTraits).isEmpty } @@ -78,6 +79,18 @@ public enum PackageDependency: Equatable, Hashable, Sendable { public var isDefaultsCase: Bool { name == "default" && condition == nil } + + /// Determines whether this trait's condition would be met by a set of enabled traits, + /// therefore enabling this trait. + /// Defaults to true if there is no condition to be satisfied. + /// + /// - Parameters: + /// - traits: A list of enabled traits. + public func isEnabled(by traits: EnabledTraits) -> Bool { + guard let condition else { return true } + + return condition.isSatisfied(by: traits.names) + } } case fileSystem(FileSystem) diff --git a/Sources/PackageModel/Manifest/TraitConfiguration.swift b/Sources/PackageModel/Manifest/TraitConfiguration.swift index 65ebe5acc51..d34f04dc4af 100644 --- a/Sources/PackageModel/Manifest/TraitConfiguration.swift +++ b/Sources/PackageModel/Manifest/TraitConfiguration.swift @@ -44,12 +44,12 @@ public enum TraitConfiguration: Codable, Hashable { } /// The set of enabled traits, if available. - public var enabledTraits: Set? { + public var enabledTraits: EnabledTraits? { switch self { case .default: ["default"] case .enabledTraits(let traits): - traits + EnabledTrait.createSet(from: traits, enabledBy: .traitConfiguration) case .disableAllTraits: [] case .enableAllTraits: diff --git a/Sources/Workspace/PackageContainer/FileSystemPackageContainer.swift b/Sources/Workspace/PackageContainer/FileSystemPackageContainer.swift index 47a19c0bb73..2517c3d7f9c 100644 --- a/Sources/Workspace/PackageContainer/FileSystemPackageContainer.swift +++ b/Sources/Workspace/PackageContainer/FileSystemPackageContainer.swift @@ -94,7 +94,7 @@ public struct FileSystemPackageContainer: PackageContainer { } } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) async throws -> [PackageContainerConstraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) async throws -> [PackageContainerConstraint] { let manifest = try await self.loadManifest() return try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits) } @@ -121,11 +121,11 @@ public struct FileSystemPackageContainer: PackageContainer { fatalError("This should never be called") } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { fatalError("This should never be called") } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { fatalError("This should never be called") } } diff --git a/Sources/Workspace/PackageContainer/RegistryPackageContainer.swift b/Sources/Workspace/PackageContainer/RegistryPackageContainer.swift index 630ba49a280..1279e0d7074 100644 --- a/Sources/Workspace/PackageContainer/RegistryPackageContainer.swift +++ b/Sources/Workspace/PackageContainer/RegistryPackageContainer.swift @@ -104,16 +104,16 @@ public class RegistryPackageContainer: PackageContainer { return results } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) async throws -> [PackageContainerConstraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) async throws -> [PackageContainerConstraint] { let manifest = try await self.loadManifest(version: version) return try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits) } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { throw InternalError("getDependencies for revision not supported by RegistryPackageContainer") } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { throw InternalError("getUnversionedDependencies not supported by RegistryPackageContainer") } diff --git a/Sources/Workspace/PackageContainer/SourceControlPackageContainer.swift b/Sources/Workspace/PackageContainer/SourceControlPackageContainer.swift index 2444ec5bfae..227a9d8ac4f 100644 --- a/Sources/Workspace/PackageContainer/SourceControlPackageContainer.swift +++ b/Sources/Workspace/PackageContainer/SourceControlPackageContainer.swift @@ -241,7 +241,7 @@ internal final class SourceControlPackageContainer: PackageContainer, CustomStri } } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) async throws -> [Constraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) async throws -> [Constraint] { do { return try await self.getCachedDependencies(forIdentifier: version.description, productFilter: productFilter) { guard let tag = try self.knownVersions()[version] else { @@ -259,7 +259,7 @@ internal final class SourceControlPackageContainer: PackageContainer, CustomStri } } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) async throws -> [Constraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) async throws -> [Constraint] { do { return try await self.getCachedDependencies(forIdentifier: revision, productFilter: productFilter) { // resolve the revision identifier and return its dependencies. @@ -323,7 +323,7 @@ internal final class SourceControlPackageContainer: PackageContainer, CustomStri tag: String, version: Version? = nil, productFilter: ProductFilter, - enabledTraits: Set + enabledTraits: EnabledTraits ) async throws -> (Manifest, [Constraint]) { let manifest = try await self.loadManifest(tag: tag, version: version) return (manifest, try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits)) @@ -334,13 +334,13 @@ internal final class SourceControlPackageContainer: PackageContainer, CustomStri at revision: Revision, version: Version? = nil, productFilter: ProductFilter, - enabledTraits: Set + enabledTraits: EnabledTraits ) async throws -> (Manifest, [Constraint]) { let manifest = try await self.loadManifest(at: revision, version: version) return (manifest, try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits)) } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [Constraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [Constraint] { // We just return an empty array if requested for unversioned dependencies. return [] } diff --git a/Sources/Workspace/ResolverPrecomputationProvider.swift b/Sources/Workspace/ResolverPrecomputationProvider.swift index 4ae40119dce..b6ad1ec31ed 100644 --- a/Sources/Workspace/ResolverPrecomputationProvider.swift +++ b/Sources/Workspace/ResolverPrecomputationProvider.swift @@ -122,7 +122,7 @@ private struct LocalPackageContainer: PackageContainer { try await self.versionsDescending() } - func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { // Because of the implementation of `reversedVersions`, we should only get the exact same version. switch dependency?.state { case .sourceControlCheckout(.version(version, revision: _)): @@ -134,7 +134,7 @@ private struct LocalPackageContainer: PackageContainer { } } - func getDependencies(at revisionString: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + func getDependencies(at revisionString: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { let revision = Revision(identifier: revisionString) switch dependency?.state { case .sourceControlCheckout(.branch(_, revision: revision)), .sourceControlCheckout(.revision(revision)): @@ -150,7 +150,7 @@ private struct LocalPackageContainer: PackageContainer { } } - func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { switch dependency?.state { case .none, .fileSystem, .edited: return try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits) diff --git a/Sources/Workspace/Workspace+Dependencies.swift b/Sources/Workspace/Workspace+Dependencies.swift index 90b2593a381..99e6d15c289 100644 --- a/Sources/Workspace/Workspace+Dependencies.swift +++ b/Sources/Workspace/Workspace+Dependencies.swift @@ -517,12 +517,12 @@ extension Workspace { // Precompute enabled traits, beginning with // root manifests, if we haven't already done so. - if self.enabledTraitsMap.dictionaryLiteral.isEmpty { - let rootManifestMap = rootManifests.values.reduce(into: [PackageIdentity: Manifest]()) { manifestMap, manifest in - manifestMap[manifest.packageIdentity] = manifest - } - self.enabledTraitsMap = .init(try precomputeTraits(rootManifests.values.map({ $0 }), rootManifestMap)) - } +// if self.enabledTraitsMap.dictionaryLiteral.isEmpty { +// let rootManifestMap = rootManifests.values.reduce(into: [PackageIdentity: Manifest]()) { manifestMap, manifest in +// manifestMap[manifest.packageIdentity] = manifest +// } +// self.enabledTraitsMap = .init(try precomputeTraits(rootManifests.values.map({ $0 }), rootManifestMap)) +// } // Load the current manifests. let graphRoot = try PackageGraphRoot( @@ -539,6 +539,11 @@ extension Workspace { observabilityScope: observabilityScope ) + // Update the traits map if we've fetched new manifests +// currentManifests.allDependencyManifests.forEach({ manifest in +// let enabledTraits = +// }) + guard !observabilityScope.errorsReported else { return currentManifests } diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index d066afa5f5f..60a464c1038 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -30,6 +30,9 @@ import struct PackageGraph.PackageGraphRoot import class PackageLoading.ManifestLoader import struct PackageLoading.ManifestValidator import struct PackageLoading.ToolsVersionParser +import struct PackageModel.EnabledTrait +import struct PackageModel.EnabledTraits +import enum PackageModel.TraitError import class PackageModel.Manifest import struct PackageModel.PackageIdentity import struct PackageModel.PackageReference @@ -542,7 +545,7 @@ extension Workspace { // Load root dependencies manifests (in parallel) let rootDependencies = root.dependencies.map(\.packageRef) try await prepopulateManagedDependencies(rootDependencies) - let rootDependenciesManifests = await self.loadManagedManifests( + let rootDependenciesManifests = try await self.loadManagedManifests( for: rootDependencies, observabilityScope: observabilityScope ) @@ -550,14 +553,19 @@ extension Workspace { let rootManifests = try root.manifests.mapValues { manifest in let parentEnabledTraits = self.enabledTraitsMap[manifest.packageIdentity] let deps = try manifest.dependencies.filter { dep in - let explicitlyEnabledTraits = dep.traits?.filter({ - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentEnabledTraits) - }).map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - self.enabledTraitsMap[dep.identity] = enabledTraitsSet + let explicitlyEnabledTraitsSet = dep.traits?.filter({ $0.isEnabled(by: parentEnabledTraits) }).map(\.name) + if let explicitlyEnabledTraitsSet { + let explicitlyEnabledTraits = EnabledTraits( + explicitlyEnabledTraitsSet, + setBy: .package(.init(manifest)) + ) + self.enabledTraitsMap[dep.identity] = explicitlyEnabledTraits } +// .map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: manifest.packageIdentity, name: manifest.displayName))) }) + +// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { +// self.enabledTraitsMap[dep.identity] = enabledTraitsSet +// } let isDepUsed = try manifest.isPackageDependencyUsed(dep, enabledTraits: parentEnabledTraits) return isDepUsed @@ -594,14 +602,16 @@ extension Workspace { // optimization: preload first level dependencies manifest (in parallel) let firstLevelDependencies = try topLevelManifests.values.map { manifest in let parentEnabledTraits = self.enabledTraitsMap[manifest.packageIdentity] + print("enabled traits for \(manifest.packageIdentity.description): \(parentEnabledTraits)") return try manifest.dependencies.filter { dep in - let explicitlyEnabledTraits = dep.traits?.filter({ - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentEnabledTraits) - }).map(\.name) + let explicitlyEnabledTraitsSet = dep.traits?.filter({ $0.isEnabled(by: parentEnabledTraits)}).map(\.name) - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - self.enabledTraitsMap[dep.identity] = enabledTraitsSet + if let explicitlyEnabledTraitsSet { + let explicitlyEnabledTraits = EnabledTraits( + explicitlyEnabledTraitsSet, + setBy: .package(.init(manifest)) + ) + self.enabledTraitsMap[dep.identity] = explicitlyEnabledTraits } let isDepUsed = try manifest.isPackageDependencyUsed(dep, enabledTraits: parentEnabledTraits) @@ -610,7 +620,7 @@ extension Workspace { }.map(\.packageRef) }.flatMap(\.self) - let firstLevelManifests = await self.loadManagedManifests( + let firstLevelManifests = try await self.loadManagedManifests( for: firstLevelDependencies, observabilityScope: observabilityScope ) @@ -631,7 +641,7 @@ extension Workspace { let dependenciesToLoad = dependenciesRequired.map(\.packageRef) .filter { !loadedManifests.keys.contains($0.identity) } try await prepopulateManagedDependencies(dependenciesToLoad) - let dependenciesManifests = await self.loadManagedManifests( + let dependenciesManifests = try await self.loadManagedManifests( for: dependenciesToLoad, observabilityScope: observabilityScope ) @@ -639,19 +649,27 @@ extension Workspace { return try dependenciesRequired.compactMap { dependency in return try loadedManifests[dependency.identity].flatMap { manifest in - let explicitlyEnabledTraits = dependency.traits?.filter { - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: node.item.enabledTraits) - }.map(\.name) + let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: node.item.enabledTraits)}.map(\.name) - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - let calculatedTraits = try manifest.enabledTraits( - using: enabledTraitsSet, - .init(node.item.manifest) +// .map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: node.item.identity, name: node.item.manifest.displayName)))}) + + if let explicitlyEnabledTraits { + let explicitlyEnabledTraits = EnabledTraits( + explicitlyEnabledTraits, + setBy: .package(.init(node.item.manifest)) ) + let calculatedTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) self.enabledTraitsMap[dependency.identity] = calculatedTraits } +// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { +// let calculatedTraits = try manifest.enabledTraits( +// using: enabledTraitsSet +//// .init(node.item.manifest) +// ) +// self.enabledTraitsMap[dependency.identity] = calculatedTraits +// } + // we also compare the location as this function may attempt to load // dependencies that have the same identity but from a different location // which is an error case we diagnose an report about in the GraphLoading part which @@ -697,7 +715,9 @@ extension Workspace { } // Update enabled traits map - self.enabledTraitsMap = .init(try precomputeTraits( topLevelManifests.values.map({ $0 }), loadedManifests)) + // TODO bp +// self.enabledTraitsMap = .init(try precomputeTraits( topLevelManifests.values.map({ $0 }), loadedManifests)) +// self.updateEnabledTraits(for: <#T##Manifest#>) let dependencyManifests = allNodes.filter { !$0.value.manifest.packageKind.isRoot } @@ -744,17 +764,17 @@ extension Workspace { private func loadManagedManifests( for packages: [PackageReference], observabilityScope: ObservabilityScope - ) async -> [PackageIdentity: Manifest] { - await withTaskGroup(of: (PackageIdentity, Manifest?).self) { group in + ) async throws -> [PackageIdentity: Manifest] { + try await withThrowingTaskGroup(of: (PackageIdentity, Manifest?).self) { group in for package in Set(packages) { group.addTask { await ( package.identity, - self.loadManagedManifest(for: package, observabilityScope: observabilityScope) + try self.loadManagedManifest(for: package, observabilityScope: observabilityScope) ) } } - return await group.compactMap { + return try await group.compactMap { $0 as? (PackageIdentity, Manifest) }.reduce(into: [PackageIdentity: Manifest]()) { partialResult, loadedManifest in partialResult[loadedManifest.0] = loadedManifest.1 @@ -766,7 +786,7 @@ extension Workspace { private func loadManagedManifest( for package: PackageReference, observabilityScope: ObservabilityScope - ) async -> Manifest? { + ) async throws -> Manifest? { // Check if this dependency is available. // we also compare the location as this function may attempt to load // dependencies that have the same identity but from a different location @@ -819,7 +839,7 @@ extension Workspace { } // Load and return the manifest. - return try? await self.loadManifest( + return try await self.loadManifest( packageIdentity: managedDependency.packageRef.identity, packageKind: packageKind, packagePath: packagePath, @@ -904,6 +924,11 @@ extension Workspace { manifestLoadingDiagnostics.append(contentsOf: validationIssues) throw Diagnostics.fatalError } + + // Upon loading a new manifest, check whether the enabledTraitsMap needs to update. + // TODO bp: to add parent here if possible + try updateEnabledTraits(for: manifest) + self.delegate?.didLoadManifest( packageIdentity: packageIdentity, packagePath: packagePath, diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift index 5811d6da9c4..b18c63c4e97 100644 --- a/Sources/Workspace/Workspace+Traits.swift +++ b/Sources/Workspace/Workspace+Traits.swift @@ -13,12 +13,58 @@ import class PackageModel.Manifest import struct PackageModel.PackageIdentity import enum PackageModel.ProductFilter +import enum PackageModel.PackageDependency +import struct PackageModel.EnabledTrait +import struct PackageModel.EnabledTraits extension Workspace { + public func updateEnabledTraits(for manifest: Manifest) throws { + let explicitlyEnabledTraits = manifest.packageKind.isRoot ? try manifest.enabledTraits(using: self.traitConfiguration) : self.enabledTraitsMap[manifest.packageIdentity] + // TODO bp set parent here, if possible, for loaded manifests that aren't root. + let enabledTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) +// print("====== package \(manifest.packageIdentity.description) ========") +// print("explicit traits: \(explicitlyEnabledTraits)") +// print("new calculated traits: \(traits)") + self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits +// print("traits in map: \(self.enabledTraitsMap[manifest.packageIdentity])") +// print(self.enabledTraitsMap) + + // Check dependencies of the manifest; see if present in enabled traits map + for dep in manifest.dependencies { + updateEnabledTraits(forDependency: dep, manifest) + } + } + + private func updateEnabledTraits(forDependency dependency: PackageDependency, _ parent: Manifest) { + let parentEnabledTraits = self.enabledTraitsMap[parent.packageIdentity] + let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: parentEnabledTraits)}.map(\.name) + + if let explicitlyEnabledTraits { + let explicitlyEnabledTraits = EnabledTraits( + explicitlyEnabledTraits, + setBy: .package(.init(parent)) + ) + self.enabledTraitsMap[dependency.identity] = explicitlyEnabledTraits + } + + // TODO bp: fetch loaded manifest for dependency, if it exists. + // otherwise, we can omit this part: +// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { +//// let calculatedTraits = try dependencyManifest.enabledTraits( +//// using: enabledTraitsSet, +//// .init(parent) +//// ) +// // just add the parent enabled traits to +// // the map; once this dependency is loaded, it will make a call to updateenabledtraits anyways +// // TODO bp see if necessary to add parent here +// self.enabledTraitsMap[dependency.identity/*, .package(.init(parent))*/] = enabledTraitsSet +// } + } + public func precomputeTraits( _ topLevelManifests: [Manifest], _ manifestMap: [PackageIdentity: Manifest] - ) throws -> [PackageIdentity: Set] { + ) throws -> [PackageIdentity: EnabledTraits] { var visited: Set = [] func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { @@ -29,18 +75,23 @@ extension Workspace { _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in return try manifestMap[dependency.identity].flatMap({ manifest in - let explicitlyEnabledTraits = dependency.traits?.filter { - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentTraits) - }.map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - let calculatedTraits = try manifest.enabledTraits( - using: enabledTraitsSet, - .init(parent) + let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: parentTraits) }.map(\.name) +// .map({ EnabledTrait(name: $0.name, setBy: .package(.init(parent))) }) + if let explicitlyEnabledTraits { + let explicitlyEnabledTraits = EnabledTraits( + explicitlyEnabledTraits, + setBy: .package(.init(parent)) ) + let calculatedTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) self.enabledTraitsMap[dependency.identity] = calculatedTraits } +// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { +// let calculatedTraits = try manifest.enabledTraits( +// using: enabledTraitsSet +//// .init(parent) +// ) +// self.enabledTraitsMap[dependency.identity] = calculatedTraits +// } let result = visited.insert(dependency.identity) if result.inserted { @@ -60,7 +111,7 @@ extension Workspace { } } + print("enabled traits map: \(enabledTraitsMap)") return self.enabledTraitsMap.dictionaryLiteral } - } diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index b5243e1a170..6aaa0096089 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -1108,10 +1108,11 @@ extension Workspace { // Store the manifest. rootManifests[package] = manifest + // TODO bp: call update enabled traits here instead? // Compute the enabled traits for roots. - let traitConfiguration = self.configuration.traitConfiguration - let enabledTraits = try manifest.enabledTraits(using: traitConfiguration) - self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits +// let traitConfiguration = self.traitConfiguration +// let enabledTraits = try manifest.enabledTraits(using: traitConfiguration) +// self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits } } @@ -1251,7 +1252,7 @@ extension Workspace { prebuilts: [:], fileSystem: self.fileSystem, observabilityScope: observabilityScope, - enabledTraits: try manifest.enabledTraits(using: .default) + enabledTraits: try manifest.enabledTraits(using: self.traitConfiguration) ) return try builder.construct() } @@ -1318,7 +1319,7 @@ extension Workspace { createREPLProduct: self.configuration.createREPLProduct, fileSystem: self.fileSystem, observabilityScope: observabilityScope, - enabledTraits: try manifest.enabledTraits(using: .default) + enabledTraits: try manifest.enabledTraits(using: self.traitConfiguration) ) return try builder.construct() } diff --git a/Sources/_InternalTestSupport/MockManifestLoader.swift b/Sources/_InternalTestSupport/MockManifestLoader.swift index 322681abde7..3ccfb3618cd 100644 --- a/Sources/_InternalTestSupport/MockManifestLoader.swift +++ b/Sources/_InternalTestSupport/MockManifestLoader.swift @@ -130,7 +130,7 @@ extension ManifestLoader { dependencyMapper: DependencyMapper? = .none, fileSystem: FileSystem, observabilityScope: ObservabilityScope - ) async throws -> Manifest{ + ) async throws -> Manifest { let packageIdentity: PackageIdentity let packageLocation: String switch packageKind { diff --git a/Sources/_InternalTestSupport/MockPackageContainer.swift b/Sources/_InternalTestSupport/MockPackageContainer.swift index 92a243f98c0..e0c87d3abb5 100644 --- a/Sources/_InternalTestSupport/MockPackageContainer.swift +++ b/Sources/_InternalTestSupport/MockPackageContainer.swift @@ -45,12 +45,12 @@ public class MockPackageContainer: CustomPackageContainer { return _versions } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) -> [MockPackageContainer.Constraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) -> [MockPackageContainer.Constraint] { requestedVersions.insert(version) return getDependencies(at: version.description, productFilter: productFilter, enabledTraits) } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) -> [MockPackageContainer.Constraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) -> [MockPackageContainer.Constraint] { let dependencies: [Dependency] if filteredMode { dependencies = filteredDependencies[productFilter]! @@ -63,7 +63,7 @@ public class MockPackageContainer: CustomPackageContainer { } } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) -> [MockPackageContainer.Constraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) -> [MockPackageContainer.Constraint] { return unversionedDeps } diff --git a/Tests/PackageGraphTests/PubGrubTests.swift b/Tests/PackageGraphTests/PubGrubTests.swift index 8f2bbcfa2dd..ba04635930d 100644 --- a/Tests/PackageGraphTests/PubGrubTests.swift +++ b/Tests/PackageGraphTests/PubGrubTests.swift @@ -3165,11 +3165,11 @@ public class MockContainer: PackageContainer { return version } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { return try getDependencies(at: version.description, productFilter: productFilter, enabledTraits) } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { guard let revisionDependencies = dependencies[revision] else { throw _MockLoadingError.unknownRevision } @@ -3183,7 +3183,7 @@ public class MockContainer: PackageContainer { }) } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { // FIXME: This is messy, remove unversionedDeps property. if !unversionedDeps.isEmpty { return unversionedDeps diff --git a/Tests/PackageModelTests/ManifestTests.swift b/Tests/PackageModelTests/ManifestTests.swift index 19b84a0c56d..5a36900e13b 100644 --- a/Tests/PackageModelTests/ManifestTests.swift +++ b/Tests/PackageModelTests/ManifestTests.swift @@ -202,8 +202,9 @@ class ManifestTests: XCTestCase { pruneDependencies: true // Since all dependencies are used, this shouldn't affect the outcome. ) + let enabledTraits = EnabledTraits(traits.map(\.name), setBy: .traitConfiguration) for trait in traits.sorted(by: { $0.name < $1.name }) { - XCTAssertThrowsError(try manifest.isTraitEnabled(trait, Set(traits.map(\.name)))) { error in + XCTAssertThrowsError(try manifest.isTraitEnabled(trait, enabledTraits)) { error in XCTAssertEqual("\(error)", """ Trait '\( trait @@ -337,14 +338,15 @@ class ManifestTests: XCTestCase { // Enabled Traits when passed explicitly enabled traits list: // If given a parent package, and the enabled traits being passed don't exist: - XCTAssertThrowsError(try manifest.enabledTraits(using: ["Trait1"], .init(identity: "qux"))) { error in + XCTAssertThrowsError(try manifest.enabledTraits(using: [EnabledTrait(name: "Trait1", setBy: .package(.init(identity: "qux")))])) { error in XCTAssertEqual("\(error)", """ Package 'qux' enables traits [Trait1] on package 'foo' (Foo) that declares no traits. """) } // If given a parent package, and the default traits are disabled: - XCTAssertThrowsError(try manifest.enabledTraits(using: [], .init(identity: "qux"))) { error in + // TODO bp need to uncover a method to deal with parent pacakge disabling dependencies' traits for this error + XCTAssertThrowsError(try manifest.enabledTraits(using: [])) { error in XCTAssertEqual("\(error)", """ Disabled default traits by package 'qux' on package 'foo' (Foo) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. """) @@ -458,8 +460,13 @@ class ManifestTests: XCTestCase { traits: traits ) + // Like the above configuration, Trait1 is on by default. When calling `isTraitEnabled`, + // it should calculate transitively enabled traits from here which would eventualy uncover + // that each trait is enabled. + let enabledTraits = EnabledTrait.createSet(from: ["Trait1"], enabledBy: .trait("default")) + for trait in traits.sorted(by: { $0.name < $1.name }) { - XCTAssertTrue(try manifest.isTraitEnabled(trait, Set(traits.map(\.name)))) + XCTAssertTrue(try manifest.isTraitEnabled(trait, enabledTraits)) } } } diff --git a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift new file mode 100644 index 00000000000..381791815cd --- /dev/null +++ b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift @@ -0,0 +1,792 @@ +// +// WorkspaceTests+Traits.swift +// SwiftPM +// +// Created by Bri Peticca on 2025-10-22. +// + +import _InternalTestSupport +import Basics +import PackageModel +import XCTest + +extension WorkspaceTests { + func testTraitConfigurationExists_NoDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + // Trait1 enabled; should be present in list of dependencies + condition: .init(traits: ["Trait1"]) + ), + .product( + name: "Boo", + package: "Boo", + // Trait2 disabled; should remove this dependency from graph + condition: .init(traits: ["Trait2"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: ["Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "Boo", + targets: [ + MockTarget(name: "Boo"), + ], + products: [ + MockProduct(name: "Boo", modules: ["Boo"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ], + // Only Trait1 is configured to be enabled; since `pruneDependencies` is false + // by default, there will be unused dependencies present + traitConfiguration: .init(enabledTraits: ["Trait1"], enableAllTraits: false) + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), + ] + + try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Baz", "Foo") + result.check(modules: "Bar", "Baz", "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Baz") } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + } + XCTAssertNoDiagnostics(diagnostics) + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + } + } + + func testTraitConfigurationExists_WithDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) + ), + .product( + name: "Boo", + package: "Boo", + condition: .init(traits: ["Trait2"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "Boo", + targets: [ + MockTarget(name: "Boo"), + ], + products: [ + MockProduct(name: "Boo", modules: ["Boo"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ], + // Trait configuration overrides default traits; all traits set to enabled. + traitConfiguration: .init(enabledTraits: [], enableAllTraits: true), + // With this configuration, no dependencies are unused so nothing should be pruned + // despite the `pruneDependencies` flag being set to true. + pruneDependencies: true + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), + .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), + ] + + try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Baz", "Foo", "Boo") + result.check(modules: "Bar", "Baz", "Boo", "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Baz", "Boo") } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + } + XCTAssertNoDiagnostics(diagnostics) + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + result.check(dependency: "boo", at: .checkout(.version("1.0.0"))) + } + } + + func testTraitConfiguration_WithPrunedDependencies() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) + ), + .product( + name: "Boo", + package: "Boo", + condition: .init(traits: ["Trait2"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), + // unused dependency due to trait guarding; should be omitted + .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), + // unused dependency; should be omitted + .sourceControl(path: "./Bam", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ], + // Trait configuration overrides default traits; no traits enabled + traitConfiguration: .init(enabledTraits: [], enableAllTraits: false), + pruneDependencies: true + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), + .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), + ] + + try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Baz", "Foo") + result.check(modules: "Bar", "Baz", "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: []) } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + } + XCTAssertNoDiagnostics(diagnostics) + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + } + } + + func testNoTraitConfiguration_WithDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) // Baz dependency guarded by traits. + ), + .product( + name: "Boo", + package: "Boo", + condition: .init(traits: ["Trait2"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), // Baz dependency not guarded by traits. + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "Boo", + targets: [ + MockTarget(name: "Boo"), + ], + products: [ + MockProduct(name: "Boo", modules: ["Boo"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), + .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), + ] + try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Baz", "Boo", "Foo") + result.check(modules: "Bar", "Baz", "Boo", "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Boo") } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + } + XCTAssertNoDiagnostics(diagnostics) + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + } + } + + func testInvalidTrait_WhenParentPackageEnablesTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0"), traits: ["TraitNotFound"]), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + traits: ["TraitFound"], + versions: ["1.0.0", "1.5.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"]), traits: ["TraitFound"]), + ] + + try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in + testDiagnostics(diagnostics) { result in + result.check(diagnostic: .equal("Trait 'TraitNotFound' enabled by parent package 'foo' (Foo) is not declared by package 'baz' (Baz). The available traits declared by this package are: TraitFound."), severity: .error) + } + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + } + } + + func testInvalidTraitConfiguration_ForRootPackage() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0"), traits: ["TraitFound"]), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + traits: ["TraitFound"], + versions: ["1.0.0", "1.5.0"] + ), + ], + // Trait configuration containing trait that isn't defined in the root package. + traitConfiguration: .enabledTraits(["TraitNotFound"]), + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"]), traits: ["TraitFound"]), + ] + + try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in + testDiagnostics(diagnostics) { result in + result.check(diagnostic: .equal("Trait 'TraitNotFound' is not declared by package 'foo' (Foo). The available traits declared by this package are: Trait1, Trait2, default."), severity: .error) + } + } + } + + func testManyTraitsEnableTargetDependency() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + func createMockWorkspace(_ traitConfiguration: TraitConfiguration) async throws -> MockWorkspace { + try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Cereal", + targets: [ + MockTarget( + name: "Wheat", + dependencies: [ + .product( + name: "Icing", + package: "Sugar", + condition: .init(traits: ["BreakfastOfChampions", "DontTellMom"]) + ), + ] + ), + ], + products: [ + MockProduct(name: "YummyBreakfast", modules: ["Wheat"]) + ], + dependencies: [ + .sourceControl(path: "./Sugar", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: ["BreakfastOfChampions", "DontTellMom"] + ), + ], + packages: [ + MockPackage( + name: "Sugar", + targets: [ + MockTarget(name: "Icing"), + ], + products: [ + MockProduct(name: "Icing", modules: ["Icing"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ], + traitConfiguration: traitConfiguration + ) + } + + + let deps: [MockDependency] = [ + .sourceControl(path: "./Sugar", requirement: .exact("1.0.0"), products: .specific(["Icing"])), + ] + + let workspaceOfChampions = try await createMockWorkspace(.enabledTraits(["BreakfastOfChampions"])) + try await workspaceOfChampions.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar") + result.check(modules: "Wheat", "Icing") + result.check(products: "YummyBreakfast", "Icing") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing") + } + } + } + + let dontTellMomAboutThisWorkspace = try await createMockWorkspace(.enabledTraits(["DontTellMom"])) + try await dontTellMomAboutThisWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar") + result.check(modules: "Wheat", "Icing") + result.check(products: "YummyBreakfast", "Icing") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing") + } + } + } + + let allEnabledTraitsWorkspace = try await createMockWorkspace(.enableAllTraits) + try await allEnabledTraitsWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar") + result.check(modules: "Wheat", "Icing") + result.check(products: "YummyBreakfast", "Icing") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing") + } + } + } + + let noSugarForBreakfastWorkspace = try await createMockWorkspace(.disableAllTraits) + try await noSugarForBreakfastWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal") + result.check(modules: "Wheat") + result.check(products: "YummyBreakfast") + } + } + } + + func testTraitsConditionalDependencies() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + func createMockWorkspace(_ traitConfiguration: TraitConfiguration) async throws -> MockWorkspace { + try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Cereal", + targets: [ + MockTarget( + name: "Wheat", + dependencies: [ + .product( + name: "Icing", + package: "Sugar", + condition: .init(traits: ["BreakfastOfChampions"]) + ), + .product( + name: "Raisin", + package: "Fruit", + condition: .init(traits: ["Healthy"]) + ) + ] + ), + ], + products: [ + MockProduct(name: "YummyBreakfast", modules: ["Wheat"]) + ], + dependencies: [ + .sourceControl(path: "./Sugar", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Fruit", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: ["Healthy", "BreakfastOfChampions"] + ), + ], + packages: [ + MockPackage( + name: "Sugar", + targets: [ + MockTarget(name: "Icing"), + ], + products: [ + MockProduct(name: "Icing", modules: ["Icing"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "Fruit", + targets: [ + MockTarget(name: "Raisin"), + ], + products: [ + MockProduct(name: "Raisin", modules: ["Raisin"]), + ], + versions: ["1.0.0", "1.2.0"] + ), + ], + traitConfiguration: traitConfiguration + ) + } + + + let deps: [MockDependency] = [ + .sourceControl(path: "./Sugar", requirement: .exact("1.0.0"), products: .specific(["Icing"])), + .sourceControl(path: "./Fruit", requirement: .exact("1.0.0"), products: .specific(["Raisin"])), + ] + + let workspaceOfChampions = try await createMockWorkspace(.enabledTraits(["BreakfastOfChampions"])) + try await workspaceOfChampions.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar") + result.check(modules: "Wheat", "Icing") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing") + } + } + } + + let healthyWorkspace = try await createMockWorkspace(.enabledTraits(["Healthy"])) + try await healthyWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "fruit") + result.check(modules: "Wheat", "Raisin") + result.check(products: "YummyBreakfast", "Raisin") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Raisin") + } + } + } + + let allEnabledTraitsWorkspace = try await createMockWorkspace(.enableAllTraits) + try await allEnabledTraitsWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar", "fruit") + result.check(modules: "Wheat", "Icing", "Raisin") + result.check(products: "YummyBreakfast", "Icing", "Raisin") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing", "Raisin") + } + } + } + + let boringBreakfastWorkspace = try await createMockWorkspace(.disableAllTraits) + try await boringBreakfastWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal") + result.check(modules: "Wheat") + result.check(products: "YummyBreakfast") + } + } + } + + func testDefaultTraitsEnabledInPackageDependency() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "MyTarget", + dependencies: [ + .product( + name: "MyProduct", + package: "PackageWithDefaultTraits", + ), + ] + ), + ], + products: [ + MockProduct(name: "RootProduct", modules: ["MyTarget"]) + ], + dependencies: [ + .sourceControl(path: "./PackageWithDefaultTraits", requirement: .upToNextMajor(from: "1.0.0")), + ], + ), + ], + packages: [ + MockPackage( + name: "PackageWithDefaultTraits", + targets: [ + MockTarget( + name: "PackageTarget", + dependencies: [ + .product( + name: "GuardedProduct", + package: "GuardedDependency", + condition: .init(traits: ["Enabled1"]) + ) + ] + ), + ], + products: [ + MockProduct(name: "MyProduct", modules: ["PackageTarget"]), + ], + dependencies: [ + .sourceControl(path: "./GuardedDependency", requirement: .upToNextMajor(from: "1.0.0")) + ], + traits: [ + "Enabled1", + "Enabled2", + TraitDescription(name: "default", enabledTraits: ["Enabled1", "Enabled2"]), + "NotEnabled" + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "GuardedDependency", + targets: [ + MockTarget( + name: "GuardedTarget" + ) + ], + products: [ + MockProduct(name: "GuardedProduct", modules: ["GuardedTarget"]) + ], + versions: ["1.0.0", "1.5.0"] + ) + ], + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./PackageWithDefaultTraits", requirement: .upToNextMajor(from: "1.0.0")), + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "RootPackage") + result.checkPackage("PackageWithDefaultTraits") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on resolved package \(package.identity.description) that is expected to have enabled traits.") + return + } + + let deps = package.dependencies + XCTAssertEqual(deps, [PackageIdentity(urlString: "./GuardedDependency")]) + XCTAssertEqual(enabledTraits, ["Enabled1", "Enabled2"]) + } + } + } + } +} diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index eaca8c6e7e9..ea2974d13c0 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -15852,690 +15852,6 @@ final class WorkspaceTests: XCTestCase { } } - func testTraitConfigurationExists_NoDefaultTraits() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - // Trait1 enabled; should be present in list of dependencies - condition: .init(traits: ["Trait1"]) - ), - .product( - name: "Boo", - package: "Boo", - // Trait2 disabled; should remove this dependency from graph - condition: .init(traits: ["Trait2"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), - .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: ["Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - MockPackage( - name: "Boo", - targets: [ - MockTarget(name: "Boo"), - ], - products: [ - MockProduct(name: "Boo", modules: ["Boo"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ], - // Only Trait1 is configured to be enabled; since `pruneDependencies` is false - // by default, there will be unused dependencies present - traitConfiguration: .init(enabledTraits: ["Trait1"], enableAllTraits: false) - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), - ] - - try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Foo") - result.check(packages: "Baz", "Foo") - result.check(modules: "Bar", "Baz", "Foo") - result.checkTarget("Foo") { result in result.check(dependencies: "Baz") } - result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } - } - XCTAssertNoDiagnostics(diagnostics) - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - } - } - - func testTraitConfigurationExists_WithDefaultTraits() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) - ), - .product( - name: "Boo", - package: "Boo", - condition: .init(traits: ["Trait2"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), - .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - MockPackage( - name: "Boo", - targets: [ - MockTarget(name: "Boo"), - ], - products: [ - MockProduct(name: "Boo", modules: ["Boo"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ], - // Trait configuration overrides default traits; all traits set to enabled. - traitConfiguration: .init(enabledTraits: [], enableAllTraits: true), - // With this configuration, no dependencies are unused so nothing should be pruned - // despite the `pruneDependencies` flag being set to true. - pruneDependencies: true - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), - .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), - ] - - try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Foo") - result.check(packages: "Baz", "Foo", "Boo") - result.check(modules: "Bar", "Baz", "Boo", "Foo") - result.checkTarget("Foo") { result in result.check(dependencies: "Baz", "Boo") } - result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } - } - XCTAssertNoDiagnostics(diagnostics) - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - result.check(dependency: "boo", at: .checkout(.version("1.0.0"))) - } - } - - func testTraitConfiguration_WithPrunedDependencies() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) - ), - .product( - name: "Boo", - package: "Boo", - condition: .init(traits: ["Trait2"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), - // unused dependency due to trait guarding; should be omitted - .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), - // unused dependency; should be omitted - .sourceControl(path: "./Bam", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ], - // Trait configuration overrides default traits; no traits enabled - traitConfiguration: .init(enabledTraits: [], enableAllTraits: false), - pruneDependencies: true - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), - .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), - ] - - try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Foo") - result.check(packages: "Baz", "Foo") - result.check(modules: "Bar", "Baz", "Foo") - result.checkTarget("Foo") { result in result.check(dependencies: []) } - result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } - } - XCTAssertNoDiagnostics(diagnostics) - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - } - } - - func testNoTraitConfiguration_WithDefaultTraits() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) // Baz dependency guarded by traits. - ), - .product( - name: "Boo", - package: "Boo", - condition: .init(traits: ["Trait2"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), // Baz dependency not guarded by traits. - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), - .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - MockPackage( - name: "Boo", - targets: [ - MockTarget(name: "Boo"), - ], - products: [ - MockProduct(name: "Boo", modules: ["Boo"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ] - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), - .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), - ] - try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Foo") - result.check(packages: "Baz", "Boo", "Foo") - result.check(modules: "Bar", "Baz", "Boo", "Foo") - result.checkTarget("Foo") { result in result.check(dependencies: "Boo") } - result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } - } - XCTAssertNoDiagnostics(diagnostics) - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - } - } - - func testInvalidTrait_WhenParentPackageEnablesTraits() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0"), traits: ["TraitNotFound"]), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - traits: ["TraitFound"], - versions: ["1.0.0", "1.5.0"] - ), - ] - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"]), traits: ["TraitFound"]), - ] - - try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in - testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Trait 'TraitNotFound' enabled by parent package 'foo' (Foo) is not declared by package 'baz' (Baz). The available traits declared by this package are: TraitFound."), severity: .error) - } - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - } - } - - func testInvalidTraitConfiguration_ForRootPackage() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0"), traits: ["TraitFound"]), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - traits: ["TraitFound"], - versions: ["1.0.0", "1.5.0"] - ), - ], - // Trait configuration containing trait that isn't defined in the root package. - traitConfiguration: .enabledTraits(["TraitNotFound"]), - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"]), traits: ["TraitFound"]), - ] - - try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in - testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Trait 'TraitNotFound' is not declared by package 'foo' (Foo). The available traits declared by this package are: Trait1, Trait2, default."), severity: .error) - } - } - } - - func testManyTraitsEnableTargetDependency() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - func createMockWorkspace(_ traitConfiguration: TraitConfiguration) async throws -> MockWorkspace { - try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Cereal", - targets: [ - MockTarget( - name: "Wheat", - dependencies: [ - .product( - name: "Icing", - package: "Sugar", - condition: .init(traits: ["BreakfastOfChampions", "DontTellMom"]) - ), - ] - ), - ], - products: [ - MockProduct(name: "YummyBreakfast", modules: ["Wheat"]) - ], - dependencies: [ - .sourceControl(path: "./Sugar", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: ["BreakfastOfChampions", "DontTellMom"] - ), - ], - packages: [ - MockPackage( - name: "Sugar", - targets: [ - MockTarget(name: "Icing"), - ], - products: [ - MockProduct(name: "Icing", modules: ["Icing"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ], - traitConfiguration: traitConfiguration - ) - } - - - let deps: [MockDependency] = [ - .sourceControl(path: "./Sugar", requirement: .exact("1.0.0"), products: .specific(["Icing"])), - ] - - let workspaceOfChampions = try await createMockWorkspace(.enabledTraits(["BreakfastOfChampions"])) - try await workspaceOfChampions.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar") - result.check(modules: "Wheat", "Icing") - result.check(products: "YummyBreakfast", "Icing") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing") - } - } - } - - let dontTellMomAboutThisWorkspace = try await createMockWorkspace(.enabledTraits(["DontTellMom"])) - try await dontTellMomAboutThisWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar") - result.check(modules: "Wheat", "Icing") - result.check(products: "YummyBreakfast", "Icing") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing") - } - } - } - - let allEnabledTraitsWorkspace = try await createMockWorkspace(.enableAllTraits) - try await allEnabledTraitsWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar") - result.check(modules: "Wheat", "Icing") - result.check(products: "YummyBreakfast", "Icing") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing") - } - } - } - - let noSugarForBreakfastWorkspace = try await createMockWorkspace(.disableAllTraits) - try await noSugarForBreakfastWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal") - result.check(modules: "Wheat") - result.check(products: "YummyBreakfast") - } - } - } - - func testTraitsConditionalDependencies() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - func createMockWorkspace(_ traitConfiguration: TraitConfiguration) async throws -> MockWorkspace { - try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Cereal", - targets: [ - MockTarget( - name: "Wheat", - dependencies: [ - .product( - name: "Icing", - package: "Sugar", - condition: .init(traits: ["BreakfastOfChampions"]) - ), - .product( - name: "Raisin", - package: "Fruit", - condition: .init(traits: ["Healthy"]) - ) - ] - ), - ], - products: [ - MockProduct(name: "YummyBreakfast", modules: ["Wheat"]) - ], - dependencies: [ - .sourceControl(path: "./Sugar", requirement: .upToNextMajor(from: "1.0.0")), - .sourceControl(path: "./Fruit", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: ["Healthy", "BreakfastOfChampions"] - ), - ], - packages: [ - MockPackage( - name: "Sugar", - targets: [ - MockTarget(name: "Icing"), - ], - products: [ - MockProduct(name: "Icing", modules: ["Icing"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - MockPackage( - name: "Fruit", - targets: [ - MockTarget(name: "Raisin"), - ], - products: [ - MockProduct(name: "Raisin", modules: ["Raisin"]), - ], - versions: ["1.0.0", "1.2.0"] - ), - ], - traitConfiguration: traitConfiguration - ) - } - - - let deps: [MockDependency] = [ - .sourceControl(path: "./Sugar", requirement: .exact("1.0.0"), products: .specific(["Icing"])), - .sourceControl(path: "./Fruit", requirement: .exact("1.0.0"), products: .specific(["Raisin"])), - ] - - let workspaceOfChampions = try await createMockWorkspace(.enabledTraits(["BreakfastOfChampions"])) - try await workspaceOfChampions.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar") - result.check(modules: "Wheat", "Icing") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing") - } - } - } - - let healthyWorkspace = try await createMockWorkspace(.enabledTraits(["Healthy"])) - try await healthyWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "fruit") - result.check(modules: "Wheat", "Raisin") - result.check(products: "YummyBreakfast", "Raisin") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Raisin") - } - } - } - - let allEnabledTraitsWorkspace = try await createMockWorkspace(.enableAllTraits) - try await allEnabledTraitsWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar", "fruit") - result.check(modules: "Wheat", "Icing", "Raisin") - result.check(products: "YummyBreakfast", "Icing", "Raisin") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing", "Raisin") - } - } - } - - let boringBreakfastWorkspace = try await createMockWorkspace(.disableAllTraits) - try await boringBreakfastWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal") - result.check(modules: "Wheat") - result.check(products: "YummyBreakfast") - } - } - } - func makeRegistryClient( packageIdentity: PackageIdentity, packageVersion: Version, From cef1b7a6123c46c81ad8a6869995cc6dc22a73db Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Wed, 29 Oct 2025 13:48:08 -0400 Subject: [PATCH 05/14] Add EnabledTrait tests + cleanup * Add new file to PackageModelTests/ for EnabledTrait tests - Tests EnabledTrait, EnabledTraits, and EnabledTraitsMap - Extensions to EnabledTraits added as helpers for testing * Cleanup some TODOs; added some other prints to help debug, will remove once ready for review + tests passing --- .../Basics/Collections/IdentifiableSet.swift | 8 + Sources/PackageGraph/ModulesGraph.swift | 2 + Sources/PackageGraph/PackageGraphRoot.swift | 2 +- Sources/PackageModel/CMakeLists.txt | 2 +- ...bledTraitsMap.swift => EnabledTrait.swift} | 172 +++++++--- .../Manifest/Manifest+Traits.swift | 32 +- .../Manifest/TraitConfiguration.swift | 4 +- Sources/Workspace/Workspace+Traits.swift | 4 + Sources/Workspace/Workspace.swift | 4 + .../_InternalTestSupport/MockWorkspace.swift | 2 + .../PackageModelTests/EnabledTraitTests.swift | 311 ++++++++++++++++++ Tests/PackageModelTests/ManifestTests.swift | 2 +- .../WorkspaceTests+Traits.swift | 2 + 13 files changed, 473 insertions(+), 74 deletions(-) rename Sources/PackageModel/{EnabledTraitsMap.swift => EnabledTrait.swift} (51%) create mode 100644 Tests/PackageModelTests/EnabledTraitTests.swift diff --git a/Sources/Basics/Collections/IdentifiableSet.swift b/Sources/Basics/Collections/IdentifiableSet.swift index 5a7f1ed3249..c60a86868ab 100644 --- a/Sources/Basics/Collections/IdentifiableSet.swift +++ b/Sources/Basics/Collections/IdentifiableSet.swift @@ -101,6 +101,14 @@ public struct IdentifiableSet: Collection { public func contains(id: Element.ID) -> Bool { self.storage.keys.contains(id) } + + public mutating func remove(id: Element.ID) -> Element? { + self.storage.removeValue(forKey: id) + } + + public mutating func remove(_ member: Element) -> Element? { + self.storage.removeValue(forKey: member.id) + } } extension OrderedDictionary where Value: Identifiable, Key == Value.ID { diff --git a/Sources/PackageGraph/ModulesGraph.swift b/Sources/PackageGraph/ModulesGraph.swift index 98b431f64ff..7515c8ebd73 100644 --- a/Sources/PackageGraph/ModulesGraph.swift +++ b/Sources/PackageGraph/ModulesGraph.swift @@ -499,6 +499,8 @@ public func loadModulesGraph( _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in return try manifestMap[dependency.identity].flatMap({ manifest in + // TODO bp can this be shortened as an initializer for EnabledTraits? + // possibly have it filter across the dependencies itself let explicitlyEnabledTraitsSet = dependency.traits?.filter({ $0.isEnabled(by: parentTraits) }).map(\.name) if let explicitlyEnabledTraitsSet { let explicitlyEnabledTraits = EnabledTraits( diff --git a/Sources/PackageGraph/PackageGraphRoot.swift b/Sources/PackageGraph/PackageGraphRoot.swift index 170c76e894f..742d032515a 100644 --- a/Sources/PackageGraph/PackageGraphRoot.swift +++ b/Sources/PackageGraph/PackageGraphRoot.swift @@ -145,7 +145,7 @@ public struct PackageGraphRoot { /// Returns the constraints imposed by root manifests + dependencies. public func constraints(_ enabledTraitsMap: EnabledTraitsMap) throws -> [PackageContainerConstraint] { - var rootEnabledTraits: Set = [] + var rootEnabledTraits: EnabledTraits = [] let constraints = self.packages.map { (identity, package) in let enabledTraits = enabledTraitsMap[identity] rootEnabledTraits.formUnion(enabledTraits) diff --git a/Sources/PackageModel/CMakeLists.txt b/Sources/PackageModel/CMakeLists.txt index aa8848b4155..cd9a4b9f8cb 100644 --- a/Sources/PackageModel/CMakeLists.txt +++ b/Sources/PackageModel/CMakeLists.txt @@ -14,7 +14,7 @@ add_library(PackageModel BuildSettings.swift DependencyMapper.swift Diagnostics.swift - EnabledTraitsMap.swift + EnabledTrait.swift IdentityResolver.swift InstalledSwiftPMConfiguration.swift Manifest/Manifest.swift diff --git a/Sources/PackageModel/EnabledTraitsMap.swift b/Sources/PackageModel/EnabledTrait.swift similarity index 51% rename from Sources/PackageModel/EnabledTraitsMap.swift rename to Sources/PackageModel/EnabledTrait.swift index b99564c5558..a3736427b38 100644 --- a/Sources/PackageModel/EnabledTraitsMap.swift +++ b/Sources/PackageModel/EnabledTrait.swift @@ -12,7 +12,10 @@ import Basics -/// A wrapper for a dictionary that stores the transitively enabled traits for each package. +/// A wrapper struct for a dictionary that stores the transitively enabled traits for each package. +/// This struct implicitly omits adding `default` traits to its storage, and returns `nil` if it there is no existing entry for +/// a given package, since if there are no explicitly enabled traits set by anything else a package will then default to its `default` traits, +/// if they exist. public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { public typealias Key = PackageIdentity public typealias Value = EnabledTraits @@ -31,14 +34,13 @@ public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { self.storage = dictionary } - public subscript(key: PackageIdentity/*, setBy: EnabledTrait.Origin = .traitConfiguration*/) -> EnabledTraits { + public subscript(key: PackageIdentity) -> EnabledTraits { get { storage[key] ?? ["default"] } set { // Omit adding "default" explicitly, since the map returns "default" // if there is no explicit traits declared. This will allow us to check // for nil entries in the stored dictionary, which tells us whether // traits have been explicitly declared. - print("adding \(newValue) traits to \(key.description)") guard newValue != ["default"] else { return } if storage[key] == nil { storage[key] = newValue @@ -57,20 +59,41 @@ public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { } } -public struct EnabledTrait: Hashable, CustomStringConvertible, ExpressibleByStringLiteral, Comparable { +// MARK: - EnabledTrait + +/// A structure representing a trait that is enabled. The package in which this is enabled on is identified in +/// the EnabledTraitsMap. +/// +/// An enabled trait is a trait that is either explicitly enabled by a user-passed trait configuration from the command line, +/// a parent package that has defined enabled traits for its dependency package(s), or by another trait (including the default case). +/// +/// An EnabledTrait is differentiated by its `name`, and all other data stored in this struct is treated as metadata for +/// convenience. When unifying two `EnabledTrait`s, it will combine the list of setters (`setBy`) if the `name`s match. +/// +public struct EnabledTrait: Identifiable, CustomStringConvertible, ExpressibleByStringLiteral, Comparable { + // Convenience typealias for a list of `Setter` + public typealias Setters = Set + + // The identifier for the trait. + public var id: String { name } + + // The name of the trait. public let name: String - public var setBy: Set = [] - public enum Origin: Hashable, CustomStringConvertible { - case `default` + // The list of setters who have enabled this trait. + public var setters: Setters = [] + + /// An enumeration that describes where a given trait was enabled. + public enum Setter: Hashable, CustomStringConvertible { +// case `default` case traitConfiguration case package(Manifest.PackageIdentifier) case trait(String) public var description: String { switch self { - case .default: - "default" +// case .default: +// "default" case .traitConfiguration: "custom trait configuration." case .package(let parent): @@ -84,43 +107,44 @@ public struct EnabledTrait: Hashable, CustomStringConvertible, ExpressibleByStri switch self { case .package(let id): return id - case .traitConfiguration, .trait, .default: + case .traitConfiguration, .trait: return nil } } + + public static var `default`: Self { + .trait("default") + } } - public init(name: String, setBy: Origin) { + public init(name: String, setBy: Setter) { self.name = name - self.setBy = [setBy] + self.setters = [setBy] + } + + public init(name: String, setBy: [Setter]) { + self.name = name + self.setters = Set(setBy) } public var parentPackages: [Manifest.PackageIdentifier] { - setBy.compactMap(\.parentPackage) + setters.compactMap(\.parentPackage) } -// public mutating func formUnion(_ otherOrigin: Set) { -// self.setBy.formUnion(otherOrigin) -// } + public var isDefault: Bool { + name == "default" + } - public func union(_ otherTrait: EnabledTrait) -> EnabledTrait? { + public func unify(_ otherTrait: EnabledTrait) -> EnabledTrait? { guard self.name == otherTrait.name else { return nil } var updatedTrait = self - updatedTrait.setBy = setBy.union(otherTrait.setBy) + updatedTrait.setters = setters.union(otherTrait.setters) return updatedTrait } - // Static helper method to create Set from a Collection of String. - public static func createSet( - from traits: C, - enabledBy origin: EnabledTrait.Origin - ) -> EnabledTraits where C.Element == String { - .init(Set(traits.map({ EnabledTrait(name: $0, setBy: origin)}))) - } - // MARK: - CustomStringConvertible public var description: String { name @@ -137,15 +161,15 @@ public struct EnabledTrait: Hashable, CustomStringConvertible, ExpressibleByStri // we know that these two objects are referring to the same trait of a package. // In this case, the two objects should be combined into one. public static func ==(lhs: EnabledTrait, rhs: EnabledTrait) -> Bool { - lhs.name == rhs.name + return lhs.name == rhs.name } public static func ==(lhs: EnabledTrait, rhs: String) -> Bool { - lhs.name == rhs + return lhs.name == rhs } public static func ==(lhs: String, rhs: EnabledTrait) -> Bool { - lhs == rhs.name + return lhs == rhs.name } // MARK: - Comparable @@ -155,26 +179,32 @@ public struct EnabledTrait: Hashable, CustomStringConvertible, ExpressibleByStri } } -// A wrapper struct for a set of `EnabledTrait` to handle special cases. +// MARK: - EnabledTraits + +/// This struct acts as a wrapper for a set of `EnabledTrait` to handle special cases. public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { public typealias Element = EnabledTrait - public typealias Index = Set.Index + public typealias Index = IdentifiableSet.Index + + private var _traits: IdentifiableSet = [] - private var _traits: Set = [] + public static var defaults: EnabledTraits { + ["default"] + } - public init(arrayLiteral elements: EnabledTrait...) { + public init(arrayLiteral elements: Element...) { for element in elements { _traits.insert(element) } } - public init(_ traits: C, setBy origin: EnabledTrait.Origin) where C.Element == String { - let traits = Set(traits.map({ EnabledTrait(name: $0, setBy: origin) })) - self.init(traits) + public init(_ traits: C, setBy origin: EnabledTrait.Setter) where C.Element == String { + let enabledTraits = traits.map({ EnabledTrait(name: $0, setBy: origin) }) + self.init(enabledTraits) } public init(_ traits: C) where C.Element == EnabledTrait { - self._traits = Set(traits) + self._traits = IdentifiableSet(traits) } public var startIndex: Index { @@ -194,27 +224,21 @@ public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { } public mutating func formUnion(_ other: EnabledTraits) { - _traits = Set( - _traits.compactMap { trait in - if let otherTrait = other.first(where: { $0 == trait }) { - return trait.union(otherTrait) - } else { - return trait - } - } - ) + self._traits = self.union(other)._traits } - public func flatMap(_ transform: (Self.Element) throws -> Self) rethrows -> Self { + public func flatMap(_ transform: (Self.Element) throws -> EnabledTraits) rethrows -> EnabledTraits { let transformedTraits = try _traits.flatMap(transform) return EnabledTraits(transformedTraits) } + public func map(_ transform: (Self.Element) throws -> Self.Element) rethrows -> EnabledTraits { + let transformedTraits = try _traits.map(transform) + return EnabledTraits(transformedTraits) + } + public func union(_ other: EnabledTraits) -> EnabledTraits { - print("enabled traits in self: \(_traits)") - print("to union with: \(other)") let unionedTraits = _traits.union(other) - print("after union: \(unionedTraits)") return EnabledTraits(unionedTraits) } @@ -222,18 +246,34 @@ public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { return _traits.remove(member) } - public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) { - return _traits.insert(newMember) + public mutating func insert(_ newMember: Element) { + _traits.insert(newMember) } public func contains(_ member: Element) -> Bool { return _traits.contains(member) } + + public static func ==(_ lhs: EnabledTraits, _ rhs: C) -> Bool where C.Element == Element { + lhs._traits.names == rhs.names + } + + public static func ==(_ lhs: C, _ rhs: EnabledTraits) -> Bool where C.Element == Element { + lhs.names == rhs._traits.names + } + + public static func ==(_ lhs: EnabledTraits, _ rhs: EnabledTraits) -> Bool { + lhs._traits.names == rhs._traits.names + } } extension Collection where Element == EnabledTrait { public func contains(_ trait: String) -> Bool { - self.map(\.name).contains(trait) + return self.map(\.name).contains(trait) + } + + public func contains(_ trait: Element) -> Bool { + return self.contains(trait.description) } public var names: Set { @@ -245,3 +285,29 @@ extension Collection where Element == EnabledTrait { } } +extension IdentifiableSet where Element == EnabledTrait { + public func union(_ other: IdentifiableSet) -> IdentifiableSet { + var updatedContents = self + for element in other { + updatedContents.insertTrait(element) + } + return updatedContents + } + + public func union(_ other: C) -> IdentifiableSet where C.Element == Element { + if let other = other as? IdentifiableSet { + return self.union(other) + } else { + return self.union(IdentifiableSet(other.map({ $0 }))) + } + } + + private mutating func insertTrait(_ member: Element) { + if let oldElement = self.remove(member), let newElement = oldElement.unify(member) { + insert(newElement) + } else { + insert(member) + } + } +} + diff --git a/Sources/PackageModel/Manifest/Manifest+Traits.swift b/Sources/PackageModel/Manifest/Manifest+Traits.swift index ac5a449c19a..2104e1ee1d4 100644 --- a/Sources/PackageModel/Manifest/Manifest+Traits.swift +++ b/Sources/PackageModel/Manifest/Manifest+Traits.swift @@ -69,7 +69,7 @@ extension Manifest { /// Validates a trait by checking that it is defined in the manifest; if not, an error is thrown. private func validateTrait(_ trait: EnabledTrait) throws { - guard trait != "default" else { + guard !trait.isDefault else { if !supportsTraits { throw TraitError.invalidTrait( package: .init(self), @@ -152,7 +152,7 @@ extension Manifest { // Get the enabled traits; if the trait configuration's `.enabledTraits` returns nil, // we know that it's the `.enableAllTraits` case, since the config does not store // all the defined traits of the manifest itself. - let enabledTraits: EnabledTraits = traitConfiguration.enabledTraits ?? EnabledTrait.createSet(from: self.traits.map(\.name), enabledBy: .traitConfiguration) + let enabledTraits: EnabledTraits = traitConfiguration.enabledTraits ?? EnabledTraits(self.traits.map(\.name), setBy: .traitConfiguration) try validateEnabledTraits(enabledTraits) } @@ -224,7 +224,7 @@ extension Manifest { var enabledTraits: EnabledTraits = [] - if let allEnabledTraits = try? calculateAllEnabledTraits(explictlyEnabledTraits: explicitlyEnabledTraits) { + if let allEnabledTraits = try? calculateAllEnabledTraits(explicitlyEnabledTraits: explicitlyEnabledTraits) { enabledTraits = allEnabledTraits } @@ -299,28 +299,28 @@ extension Manifest { } // Compute all transitively enabled traits. - let allEnabledTraits = try calculateAllEnabledTraits(explictlyEnabledTraits: enabledTraits) + let allEnabledTraits = try calculateAllEnabledTraits(explicitlyEnabledTraits: enabledTraits) return allEnabledTraits.contains(trait.name) } /// Calculates and returns a set of all enabled traits, beginning with a set of explicitly enabled traits (which can either be the default traits of a manifest, or a configuration of enabled traits determined from a user-generated trait configuration) and determines which traits are transitively enabled. private func calculateAllEnabledTraits( - explictlyEnabledTraits: EnabledTraits, + explicitlyEnabledTraits: EnabledTraits, // _ parentPackage: PackageIdentifier? = nil ) throws -> EnabledTraits { - try validateEnabledTraits(explictlyEnabledTraits) + try validateEnabledTraits(explicitlyEnabledTraits) // This the point where we flatten the enabled traits and resolve the recursive traits - var enabledTraits = explictlyEnabledTraits - let areDefaultsEnabled = enabledTraits.remove("default") != nil // TODO bp check if this remove is ok + var enabledTraits = explicitlyEnabledTraits + let areDefaultsEnabled = enabledTraits.remove("default") != nil // We have to enable all default traits if no traits are enabled or the defaults are explicitly enabled - if explictlyEnabledTraits == ["default"] || areDefaultsEnabled { + if explicitlyEnabledTraits == ["default"] || areDefaultsEnabled { if let defaultTraits { - let transitiveDefaultTraits = EnabledTrait.createSet( - from: defaultTraits, - enabledBy: .trait("default") - ) + let transitiveDefaultTraits = EnabledTraits( + defaultTraits, + setBy: .default + ) enabledTraits.formUnion(transitiveDefaultTraits) } } @@ -339,9 +339,9 @@ extension Manifest { trait: trait ) } - return EnabledTrait.createSet( - from: traitDescription.enabledTraits, - enabledBy: .trait(traitDescription.name) + return EnabledTraits( + traitDescription.enabledTraits, + setBy: .trait(traitDescription.name) ) } diff --git a/Sources/PackageModel/Manifest/TraitConfiguration.swift b/Sources/PackageModel/Manifest/TraitConfiguration.swift index d34f04dc4af..10614ea2073 100644 --- a/Sources/PackageModel/Manifest/TraitConfiguration.swift +++ b/Sources/PackageModel/Manifest/TraitConfiguration.swift @@ -47,9 +47,9 @@ public enum TraitConfiguration: Codable, Hashable { public var enabledTraits: EnabledTraits? { switch self { case .default: - ["default"] + EnabledTraits.defaults case .enabledTraits(let traits): - EnabledTrait.createSet(from: traits, enabledBy: .traitConfiguration) + EnabledTraits(traits, setBy: .traitConfiguration) case .disableAllTraits: [] case .enableAllTraits: diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift index b18c63c4e97..5c63843045c 100644 --- a/Sources/Workspace/Workspace+Traits.swift +++ b/Sources/Workspace/Workspace+Traits.swift @@ -19,9 +19,13 @@ import struct PackageModel.EnabledTraits extension Workspace { public func updateEnabledTraits(for manifest: Manifest) throws { + print("calling update enabled traits on \(manifest.displayName)") let explicitlyEnabledTraits = manifest.packageKind.isRoot ? try manifest.enabledTraits(using: self.traitConfiguration) : self.enabledTraitsMap[manifest.packageIdentity] // TODO bp set parent here, if possible, for loaded manifests that aren't root. + print("updating traits for manifest \(manifest.displayName)") let enabledTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) + print("new enabled traits: \(enabledTraits)") + print("with map: \(enabledTraitsMap)") // print("====== package \(manifest.packageIdentity.description) ========") // print("explicit traits: \(explicitlyEnabledTraits)") // print("new calculated traits: \(traits)") diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index 6aaa0096089..0f7dcdcdc64 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -1097,6 +1097,10 @@ extension Workspace { ) return (package, manifest) } catch { + if let error = error as? TraitError { + print("trait error; please propagate") + throw error + } return nil } } diff --git a/Sources/_InternalTestSupport/MockWorkspace.swift b/Sources/_InternalTestSupport/MockWorkspace.swift index 4e8b202654f..ffdc3fc309b 100644 --- a/Sources/_InternalTestSupport/MockWorkspace.swift +++ b/Sources/_InternalTestSupport/MockWorkspace.swift @@ -624,6 +624,8 @@ public final class MockWorkspace { expectedSigningEntities: expectedSigningEntities, observabilityScope: observability.topScope ) + print("mock enabled traits map: \(self.enabledTraitsMap)") + print("real enabled traits map: \(workspace.enabledTraitsMap)") try result(graph, observability.diagnostics) } catch { // helpful when graph fails to load diff --git a/Tests/PackageModelTests/EnabledTraitTests.swift b/Tests/PackageModelTests/EnabledTraitTests.swift new file mode 100644 index 00000000000..d0b83ea9469 --- /dev/null +++ b/Tests/PackageModelTests/EnabledTraitTests.swift @@ -0,0 +1,311 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing +import struct PackageModel.EnabledTrait +import struct PackageModel.EnabledTraits +import struct PackageModel.PackageIdentity +import class PackageModel.Manifest + +@Suite( + +) +struct EnabledTraitTests { + + // MARK: - EnabledTrait Tests + @Test + func enabledTrait_checkEquality() { + let appleTraitSetByApplePie = EnabledTrait.init(name: "Apple", setBy: .package(.init(identity: "ApplePie"))) + let appleTraitSetByAppleJuice = EnabledTrait.init(name: "Apple", setBy: .trait("AppleJuice")) + let appleCoreTrait = EnabledTrait.init(name: "AppleCore", setBy: .default) + + #expect(appleTraitSetByApplePie == appleTraitSetByAppleJuice) + #expect(appleCoreTrait != appleTraitSetByApplePie) + #expect(appleCoreTrait != appleTraitSetByAppleJuice) + } + + @Test + func enabledTrait_unifyEqualTraits() throws { + let bananaTraitSetByFruit = EnabledTrait(name: "Banana", setBy: .package(.init(identity: "Fruit"))) + let bananaTraitSetByBread = EnabledTrait(name: "Banana", setBy: .trait("Bread")) + + let unifiedBananaTrait = try #require(bananaTraitSetByBread.unify(bananaTraitSetByFruit)) + let setters: Set = [ + EnabledTrait.Setter.package(.init(identity: "Fruit")), + EnabledTrait.Setter.trait(.init("Bread")) + ] + + #expect(unifiedBananaTrait.setters == setters) + } + + @Test + func enabledTrait_unifyDifferentTraits() { + let bananaTrait = EnabledTrait(name: "Banana", setBy: .package(.init(identity: "Fruit"))) + let appleTrait = EnabledTrait(name: "Apple", setBy: .package(.init(identity: "Fruit"))) + + let unifiedTrait = bananaTrait.unify(appleTrait) + + #expect(unifiedTrait == nil) + #expect(bananaTrait.setters == appleTrait.setters) + } + + @Test + func enabledTrait_compareToStringLiteral() { + let appleTrait = EnabledTrait(name: "Apple", setBy: .default) + + #expect("Apple" == appleTrait) // test when EnabledTrait rhs + #expect(appleTrait == "Apple") // test when EnabledTrait lhs + } + + @Test + func enabledTrait_initializedByStringLiteral() { + let appleTraitByString: EnabledTrait = "Apple" + let appleTraitByInit = EnabledTrait(name: "Apple", setBy: .default) + + #expect(appleTraitByString == appleTraitByInit) + } + + @Test + func enabledTrait_assertIdIsName() { + let appleTrait = EnabledTrait(name: "Apple", setBy: .default) + + #expect(appleTrait.id == appleTrait.name) + } + + @Test + func enabledTrait_CheckIfDefault() { + let defaultTrait: EnabledTrait = "default" + + #expect(defaultTrait.isDefault) + } + + @Test + func enabledTrait_SortAndCompare() { + let appleTrait: EnabledTrait = "Apple" + let bananaTrait: EnabledTrait = "Banana" + let orangeTrait: EnabledTrait = "Orange" + + let traits = [orangeTrait, appleTrait, bananaTrait] + let sortedTraits = traits.sorted() + + #expect(sortedTraits == [appleTrait, bananaTrait, orangeTrait]) + #expect(sortedTraits != traits) + #expect(appleTrait < bananaTrait) + #expect(orangeTrait > bananaTrait) + } + + @Test + func enabledTrait_getParentPackageSetters() throws { + let traitSetByPackages = EnabledTrait( + name: "Coffee", + setBy: [ + .package(.init(identity: "Cafe")), + .package(.init(identity:"Home")), + .package(.init(identity: "Breakfast")), + .trait("NotAPackage"), + .traitConfiguration + ]) + + let parentPackagesFromTrait = traitSetByPackages.parentPackages + let parentPackages = Set([ + .init(identity: "Cafe"), + .init(identity: "Home"), + .init(identity: "Breakfast") + ]) + + #expect(Set(parentPackagesFromTrait) == parentPackages) + } + + // MARK: - EnabledTraits Tests + + @Test + func enabledTraits_initWithStrings() { + let enabledTraits: EnabledTraits = ["One", "Two", "Three"] + let toTestAgainst = EnabledTraits([ + EnabledTrait(name: "One", setBy: .default), + EnabledTrait(name: "Two", setBy: .default), + EnabledTrait(name: "Three", setBy: .default) + ]) + + #expect(enabledTraits == toTestAgainst) + } + + @Test + func enabledTraits_defaultSet() { + let defaults: EnabledTraits = .defaults + + #expect(defaults == ["default"]) + #expect(defaults == [EnabledTrait(name: "default", setBy: .default)]) + } + + @Test + func enabledTraits_containsTrait() { + let enabledTraits: EnabledTraits = ["Apple", "Banana"] + + // Test against a string literal + #expect(enabledTraits.contains("Apple")) + // Test against an explicitly initialized EnabledTrait + #expect(enabledTraits.contains(EnabledTrait(name: "Apple", setBy: .default))) + // Test against string literal that is not in the set + #expect(!enabledTraits.contains("Orange")) + // Test against initialized EnabledTrait that is not in the set + #expect(!enabledTraits.contains(EnabledTrait(name: "Pineapple", setBy: .trait("Apple")))) + } + + @Test + func enabledTraits_insertAndRemoveExistingTrait() throws { + var enabledTraits: EnabledTraits = ["Apple", "Banana", "Orange"] + + let newTrait = EnabledTrait(name: "Apple", setBy: [.package(.init(identity: "Fruit")), .trait("FavouriteFruit")]) + + // Assert amount of elements before adding trait + #expect(enabledTraits.count == 3) + + // Insert trait; this should update the existing "Apple" trait by unifying its setters + enabledTraits.insert(newTrait) + #expect(enabledTraits.count == 3) + #expect(enabledTraits == ["Apple", "Banana", "Orange"]) + + + // Assure that Apple trait is removed and returned + let appleTrait = enabledTraits.remove("Apple") + let unwrappedAppleTrait = try #require(appleTrait) + #expect(enabledTraits.count == 2) + #expect(!enabledTraits.contains("Apple")) + + // Assure that Apple trait now has updated setters + #expect(unwrappedAppleTrait.setters == [.package(.init(identity: "Fruit")), .trait("FavouriteFruit")]) + + // Insert trait via string literal + enabledTraits.insert("MyStringTrait") + #expect(enabledTraits.count == 3) + #expect(enabledTraits.contains("MyStringTrait")) + } + + @Test + func enabledTraits_insertAndRemoveNonExistingTrait() throws { + var enabledTraits: EnabledTraits = ["Banana"] + + let newTrait = EnabledTrait(name: "Apple", setBy: [.package(.init(identity: "Fruit")), .trait("FavouriteFruit")]) + + + // Try to remove Apple trait before inserting: + #expect(enabledTraits.remove("Apple") == nil) + #expect(enabledTraits.count == 1) + + // Insert trait + enabledTraits.insert(newTrait) + #expect(enabledTraits.count == 2) + #expect(enabledTraits.contains("Apple")) + } + + @Test + func enabledTraits_flatMapAgainstSetOfTraits() { + let enabledTraits: EnabledTraits = ["Apple", "Coffee", "Cookie"] + let transformedTraits = enabledTraits.map({ oldTrait in + var newTrait = oldTrait + newTrait.setters.insert(.package(.init(identity: "Breakfast"))) + return newTrait + }) + + #expect( + transformedTraits == EnabledTraits([ + EnabledTrait(name: "Apple", setBy: .package(.init(identity: "Breakfast"))), + EnabledTrait(name: "Coffee", setBy: .package(.init(identity: "Breakfast"))), + EnabledTrait(name: "Cookie", setBy: .package(.init(identity: "Breakfast"))) + ]) + ) + } + + @Test + func enabledTraits_unionWithNewTraits() { + let enabledTraits: EnabledTraits = ["Banana"] + let newTraits: EnabledTraits = ["Cookie", "Pancakes", "Milkshake"] + + let unifiedSetOfTraits = enabledTraits.union(newTraits) + + #expect(unifiedSetOfTraits.count == 4) + #expect(unifiedSetOfTraits == ["Banana", "Cookie", "Pancakes", "Milkshake"]) + } + + @Test + func enabledTraits_unionWithExistingTraits() throws { + let enabledTraits: EnabledTraits = [ + EnabledTrait(name: "Banana", setBy: .default), + EnabledTrait(name: "Apple", setBy: .package(.init(identity: "MyFruits"))) + ] + let newTraits: EnabledTraits = [ + EnabledTrait(name: "Banana", setBy: [.package(.init(identity: "OtherFruits")), .trait("Bread")]), + EnabledTrait(name: "Apple", setBy: .default), + "Milkshake" + ] + + var unifiedSetOfTraits = enabledTraits.union(newTraits) + + #expect(unifiedSetOfTraits.count == 3) + #expect(unifiedSetOfTraits == ["Banana", "Apple", "Milkshake"]) + + // Check each of the setters for each enabled trait, and assure + // that they can be succesfully removed from the set. + let bananaTrait = try unifiedSetOfTraits.unwrapRemove("Banana") + + #expect(unifiedSetOfTraits.count == 2) + #expect( + bananaTrait.setters == Set([ + .package(.init(identity: "OtherFruits")), + .trait("Bread"), + .default + ]) + ) + + let appleTrait = try unifiedSetOfTraits.unwrapRemove(EnabledTrait(name: "Apple", setBy: .default)) + #expect(unifiedSetOfTraits.count == 1) + #expect( + appleTrait.setters == Set([ + .package(.init(identity: "MyFruits")), + .default + ]) + ) + + let milkshakeTrait = try unifiedSetOfTraits.unwrapRemove("Milkshake") + #expect(unifiedSetOfTraits.isEmpty) + #expect(milkshakeTrait.setters.isEmpty) + } + + @Test + func enabledTraits_testInitWithArrayOfSameString() throws { + var traits: EnabledTraits = [ + "Banana", + EnabledTrait(name: "Banana", setBy: .default), + "Chocolate" + ] + + #expect(traits.count == 2) + #expect(traits.contains("Banana")) + #expect(traits.contains("Chocolate")) + + let bananaTrait = try traits.unwrapRemove("Banana") + } + + // MARK: - EnabledTraitsMap Tests +} + + +// MARK: - Test Helpers +extension EnabledTraits { + // Helper method to unwrap elements that are removed from the set. + package mutating func unwrapRemove(_ trait: Element) throws -> Element { + let optionalTrait = self.remove(trait) + let trait = try #require(optionalTrait) + return trait + } +} diff --git a/Tests/PackageModelTests/ManifestTests.swift b/Tests/PackageModelTests/ManifestTests.swift index 5a36900e13b..33926c5780d 100644 --- a/Tests/PackageModelTests/ManifestTests.swift +++ b/Tests/PackageModelTests/ManifestTests.swift @@ -463,7 +463,7 @@ class ManifestTests: XCTestCase { // Like the above configuration, Trait1 is on by default. When calling `isTraitEnabled`, // it should calculate transitively enabled traits from here which would eventualy uncover // that each trait is enabled. - let enabledTraits = EnabledTrait.createSet(from: ["Trait1"], enabledBy: .trait("default")) + let enabledTraits = EnabledTraits(["Trait1"], setBy: .trait("default")) for trait in traits.sorted(by: { $0.name < $1.name }) { XCTAssertTrue(try manifest.isTraitEnabled(trait, enabledTraits)) diff --git a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift index 381791815cd..0c430305d45 100644 --- a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift +++ b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift @@ -782,6 +782,8 @@ extension WorkspaceTests { return } + print("enabled traits: \(enabledTraits)") + let deps = package.dependencies XCTAssertEqual(deps, [PackageIdentity(urlString: "./GuardedDependency")]) XCTAssertEqual(enabledTraits, ["Enabled1", "Enabled2"]) From 6416e54e5f9055ee6d864755ea7861456dc97aca Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Wed, 29 Oct 2025 16:52:00 -0400 Subject: [PATCH 06/14] Remove debug prints, cleanup repeated code, refactor Manifest+Traits methods --- Sources/PackageModel/EnabledTrait.swift | 29 +++- .../Manifest/Manifest+Traits.swift | 73 +++------ Sources/Workspace/Workspace+Manifests.swift | 45 +++--- Sources/Workspace/Workspace+Traits.swift | 143 +++++++++--------- Sources/Workspace/Workspace.swift | 8 +- .../_InternalTestSupport/MockWorkspace.swift | 2 - 6 files changed, 129 insertions(+), 171 deletions(-) diff --git a/Sources/PackageModel/EnabledTrait.swift b/Sources/PackageModel/EnabledTrait.swift index a3736427b38..79e58e4bd0a 100644 --- a/Sources/PackageModel/EnabledTrait.swift +++ b/Sources/PackageModel/EnabledTrait.swift @@ -20,7 +20,8 @@ public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { public typealias Key = PackageIdentity public typealias Value = EnabledTraits - var storage: [PackageIdentity: EnabledTraits] = [:] + var storage: ThreadSafeKeyValueStore = .init() +// var storage: [PackageIdentity: EnabledTraits] = [:] public init() { } @@ -31,7 +32,7 @@ public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { } public init(_ dictionary: [Key: Value]) { - self.storage = dictionary + self.storage = .init(dictionary) } public subscript(key: PackageIdentity) -> EnabledTraits { @@ -55,7 +56,7 @@ public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { } public var dictionaryLiteral: [PackageIdentity: EnabledTraits] { - return storage + return storage.get() } } @@ -85,15 +86,12 @@ public struct EnabledTrait: Identifiable, CustomStringConvertible, ExpressibleBy /// An enumeration that describes where a given trait was enabled. public enum Setter: Hashable, CustomStringConvertible { -// case `default` case traitConfiguration case package(Manifest.PackageIdentifier) case trait(String) public var description: String { switch self { -// case .default: -// "default" case .traitConfiguration: "custom trait configuration." case .package(let parent): @@ -242,6 +240,16 @@ public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { return EnabledTraits(unionedTraits) } + public func intersection(_ other: C) -> EnabledTraits where C.Element == Self.Element { + let otherSet = IdentifiableSet(other.map({ $0 })) + let intersection = self._traits.intersection(otherSet) + return EnabledTraits(intersection) + } + + public func intersection(_ other: C) -> EnabledTraits where C.Element == String { + self.intersection(other.map(\.asEnabledTrait)) + } + public mutating func remove(_ member: Element) -> Element? { return _traits.remove(member) } @@ -311,3 +319,12 @@ extension IdentifiableSet where Element == EnabledTrait { } } +package protocol EnabledTraitConvertible { + var asEnabledTrait: EnabledTrait { get } +} + +extension String: EnabledTraitConvertible { + package var asEnabledTrait: EnabledTrait { + .init(stringLiteral: self) + } +} diff --git a/Sources/PackageModel/Manifest/Manifest+Traits.swift b/Sources/PackageModel/Manifest/Manifest+Traits.swift index 2104e1ee1d4..363a4331f64 100644 --- a/Sources/PackageModel/Manifest/Manifest+Traits.swift +++ b/Sources/PackageModel/Manifest/Manifest+Traits.swift @@ -271,33 +271,6 @@ extension Manifest { return false } - // Special case for dealing with whether a default trait is enabled. - guard !trait.isDefault else { - // Check that the manifest defines default traits. - if self.traits.contains(where: \.isDefault) { - // If the trait is a default trait, then we must do the following checks: - // - If there exists a list of enabled traits, ensure that the default trait - // is declared in the set. - // - If there is no existing list of enabled traits (nil), and we know that the - // manifest has defined default traits, then just return true. - // - If none of these conditions are met, then defaults aren't enabled and we return false. - if enabledTraits.contains(trait.name) { - return true - } else if enabledTraits.isEmpty { - return true - } else { - return false - } - } - - // If manifest does not define default traits, then throw an invalid trait error. - throw TraitError.invalidTrait( - package: .init(self), - trait: .init(stringLiteral: trait.name), - availableTraits: self.traits.map(\.name) - ) - } - // Compute all transitively enabled traits. let allEnabledTraits = try calculateAllEnabledTraits(explicitlyEnabledTraits: enabledTraits) @@ -305,10 +278,7 @@ extension Manifest { } /// Calculates and returns a set of all enabled traits, beginning with a set of explicitly enabled traits (which can either be the default traits of a manifest, or a configuration of enabled traits determined from a user-generated trait configuration) and determines which traits are transitively enabled. - private func calculateAllEnabledTraits( - explicitlyEnabledTraits: EnabledTraits, -// _ parentPackage: PackageIdentifier? = nil - ) throws -> EnabledTraits { + private func calculateAllEnabledTraits(explicitlyEnabledTraits: EnabledTraits) throws -> EnabledTraits { try validateEnabledTraits(explicitlyEnabledTraits) // This the point where we flatten the enabled traits and resolve the recursive traits var enabledTraits = explicitlyEnabledTraits @@ -372,8 +342,7 @@ extension Manifest { try validateEnabledTraits(enabledTraits) } // For each trait that is a condition on this target dependency, assure that - // each one is enabled in the manifest. -// return try traits.allSatisfy({ try isTraitEnabled(.init(stringLiteral: $0), enabledTraits) }) + // at least one is enabled in the manifest. return !traits.intersection(enabledTraits.names).isEmpty } @@ -469,9 +438,9 @@ extension Manifest { return traitsToEnable.isEmpty || isEnabled } + /// Determines whether a given package dependency is used by this manifest given a set of enabled traits. public func isPackageDependencyUsed(_ dependency: PackageDependency, enabledTraits: EnabledTraits) throws -> Bool { - let isTraitGuarded = try isTraitGuarded(dependency, enabledTraits: enabledTraits) if self.pruneDependencies { let usedDependencies = try self.usedDependencies(withTraits: enabledTraits) let foundKnownPackage = usedDependencies.knownPackage.contains(where: { @@ -482,27 +451,12 @@ extension Manifest { // tentatively marking the package dependency as used. to be resolved later on. return foundKnownPackage || (!foundKnownPackage && !usedDependencies.unknownPackage.isEmpty) } else { - return !isTraitGuarded - // alternate path to compute trait-guarded package dependencies if the prune deps feature is not enabled -// try validateEnabledTraits(enabledTraits) -// -// let targetDependenciesForPackageDependency = self.targets.flatMap({ $0.dependencies }) -// .filter({ -// $0.package?.caseInsensitiveCompare(dependency.identity.description) == .orderedSame -// }) -// -// // if target deps is empty, default to returning true here. -// let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({ -// let isGuarded = $0.intersection(enabledTraits).isEmpty -// return isGuarded -// }) -// -// let isUsedWithoutTraitGuarding = !targetDependenciesForPackageDependency.filter({ $0.condition?.traits == nil }).isEmpty -// -// return isUsedWithoutTraitGuarding || !isTraitGuarded + return try !isTraitGuarded(dependency, enabledTraits: enabledTraits) } } + /// Given a set of enabled traits, determine whether a package dependecy of this manifest is + /// guarded by traits. private func isTraitGuarded(_ dependency: PackageDependency, enabledTraits: EnabledTraits) throws -> Bool { try validateEnabledTraits(enabledTraits) @@ -515,9 +469,7 @@ extension Manifest { // Determine whether the current set of enabled traits still gate the package dependency // across targets. - let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({ - let isGuarded = $0.intersection(enabledTraitNames).isEmpty - return isGuarded + let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.filter({ $0.condition?.traits != nil }).allSatisfy({ self.isTraitGuarded($0, enabledTraits: enabledTraits) }) // Since we only omit a package dependency that is only guarded by traits, determine @@ -526,6 +478,17 @@ extension Manifest { return !isUsedWithoutTraitGuarding && isTraitGuarded } + + private func isTraitGuarded( + _ dependency: TargetDescription.Dependency, + enabledTraits: EnabledTraits + ) -> Bool { + // Validate enabled traits +// try validateEnabledTraits(enabledTraits) + guard let condition = dependency.condition, let traits = condition.traits else { return false } + + return enabledTraits.intersection(traits).isEmpty + } } // MARK: - Trait Error diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 60a464c1038..46e11e4bac0 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -553,14 +553,14 @@ extension Workspace { let rootManifests = try root.manifests.mapValues { manifest in let parentEnabledTraits = self.enabledTraitsMap[manifest.packageIdentity] let deps = try manifest.dependencies.filter { dep in - let explicitlyEnabledTraitsSet = dep.traits?.filter({ $0.isEnabled(by: parentEnabledTraits) }).map(\.name) - if let explicitlyEnabledTraitsSet { - let explicitlyEnabledTraits = EnabledTraits( - explicitlyEnabledTraitsSet, - setBy: .package(.init(manifest)) - ) - self.enabledTraitsMap[dep.identity] = explicitlyEnabledTraits - } +// let explicitlyEnabledTraitsSet = dep.traits?.filter({ $0.isEnabled(by: parentEnabledTraits) }).map(\.name) +// if let explicitlyEnabledTraitsSet { +// let explicitlyEnabledTraits = EnabledTraits( +// explicitlyEnabledTraitsSet, +// setBy: .package(.init(manifest)) +// ) +// self.enabledTraitsMap[dep.identity] = explicitlyEnabledTraits +// } // .map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: manifest.packageIdentity, name: manifest.displayName))) }) // if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { @@ -602,7 +602,6 @@ extension Workspace { // optimization: preload first level dependencies manifest (in parallel) let firstLevelDependencies = try topLevelManifests.values.map { manifest in let parentEnabledTraits = self.enabledTraitsMap[manifest.packageIdentity] - print("enabled traits for \(manifest.packageIdentity.description): \(parentEnabledTraits)") return try manifest.dependencies.filter { dep in let explicitlyEnabledTraitsSet = dep.traits?.filter({ $0.isEnabled(by: parentEnabledTraits)}).map(\.name) @@ -649,18 +648,18 @@ extension Workspace { return try dependenciesRequired.compactMap { dependency in return try loadedManifests[dependency.identity].flatMap { manifest in - let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: node.item.enabledTraits)}.map(\.name) +// let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: node.item.enabledTraits)}.map(\.name) // .map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: node.item.identity, name: node.item.manifest.displayName)))}) - if let explicitlyEnabledTraits { - let explicitlyEnabledTraits = EnabledTraits( - explicitlyEnabledTraits, - setBy: .package(.init(node.item.manifest)) - ) - let calculatedTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) - self.enabledTraitsMap[dependency.identity] = calculatedTraits - } +// if let explicitlyEnabledTraits { +// let explicitlyEnabledTraits = EnabledTraits( +// explicitlyEnabledTraits, +// setBy: .package(.init(node.item.manifest)) +// ) +// let calculatedTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) +// self.enabledTraitsMap[dependency.identity] = calculatedTraits +// } // if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { // let calculatedTraits = try manifest.enabledTraits( @@ -714,11 +713,6 @@ extension Workspace { } } - // Update enabled traits map - // TODO bp -// self.enabledTraitsMap = .init(try precomputeTraits( topLevelManifests.values.map({ $0 }), loadedManifests)) -// self.updateEnabledTraits(for: <#T##Manifest#>) - let dependencyManifests = allNodes.filter { !$0.value.manifest.packageKind.isRoot } // TODO: this check should go away when introducing explicit overrides @@ -925,9 +919,8 @@ extension Workspace { throw Diagnostics.fatalError } - // Upon loading a new manifest, check whether the enabledTraitsMap needs to update. - // TODO bp: to add parent here if possible - try updateEnabledTraits(for: manifest) + // Upon loading a new manifest, update enabled traits. + try self.updateEnabledTraits(for: manifest) self.delegate?.didLoadManifest( packageIdentity: packageIdentity, diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift index 5c63843045c..27f07039a83 100644 --- a/Sources/Workspace/Workspace+Traits.swift +++ b/Sources/Workspace/Workspace+Traits.swift @@ -18,27 +18,34 @@ import struct PackageModel.EnabledTrait import struct PackageModel.EnabledTraits extension Workspace { + /// Given a loaded `Manifest`, determine the traits that are enabled for it and + /// calculate whichever traits are enabled transitively from this, if possible, and update the + /// map of enabled traits on `Workspace` (`Workspace.enabledTraitsMap`). + /// + /// If the package defines a dependency with an explicit set of enabled traits, it will also + /// add them to the enabled traits map. public func updateEnabledTraits(for manifest: Manifest) throws { - print("calling update enabled traits on \(manifest.displayName)") - let explicitlyEnabledTraits = manifest.packageKind.isRoot ? try manifest.enabledTraits(using: self.traitConfiguration) : self.enabledTraitsMap[manifest.packageIdentity] - // TODO bp set parent here, if possible, for loaded manifests that aren't root. - print("updating traits for manifest \(manifest.displayName)") + // If the `Manifest` is a root, then we should default to using + // the trait configuration set in the `Workspace`. Otherwise, + // check the enabled traits map to see if there are traits + // that have already been recorded as enabled. + let explicitlyEnabledTraits = manifest.packageKind.isRoot ? + try manifest.enabledTraits(using: self.traitConfiguration) : + self.enabledTraitsMap[manifest.packageIdentity] + let enabledTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) - print("new enabled traits: \(enabledTraits)") - print("with map: \(enabledTraitsMap)") -// print("====== package \(manifest.packageIdentity.description) ========") -// print("explicit traits: \(explicitlyEnabledTraits)") -// print("new calculated traits: \(traits)") self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits -// print("traits in map: \(self.enabledTraitsMap[manifest.packageIdentity])") -// print(self.enabledTraitsMap) - // Check dependencies of the manifest; see if present in enabled traits map + // Check enabled traits for the dependencies for dep in manifest.dependencies { updateEnabledTraits(forDependency: dep, manifest) } } + /// Update the enabled traits for a `PackageDependency` of a given parent `Manifest`. + /// + /// This is only called if a loaded `Manifest` has package dependencies in which it sets + /// an explicit list of enabled traits for that dependency. private func updateEnabledTraits(forDependency dependency: PackageDependency, _ parent: Manifest) { let parentEnabledTraits = self.enabledTraitsMap[parent.packageIdentity] let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: parentEnabledTraits)}.map(\.name) @@ -50,72 +57,58 @@ extension Workspace { ) self.enabledTraitsMap[dependency.identity] = explicitlyEnabledTraits } - - // TODO bp: fetch loaded manifest for dependency, if it exists. - // otherwise, we can omit this part: -// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { -//// let calculatedTraits = try dependencyManifest.enabledTraits( -//// using: enabledTraitsSet, -//// .init(parent) -//// ) -// // just add the parent enabled traits to -// // the map; once this dependency is loaded, it will make a call to updateenabledtraits anyways -// // TODO bp see if necessary to add parent here -// self.enabledTraitsMap[dependency.identity/*, .package(.init(parent))*/] = enabledTraitsSet -// } } - public func precomputeTraits( - _ topLevelManifests: [Manifest], - _ manifestMap: [PackageIdentity: Manifest] - ) throws -> [PackageIdentity: EnabledTraits] { - var visited: Set = [] - - func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { - let parentTraits = self.enabledTraitsMap[parent.packageIdentity] - let requiredDependencies = try parent.dependenciesRequired(for: productFilter, parentTraits) - let guardedDependencies = parent.dependenciesTraitGuarded(withEnabledTraits: parentTraits) - - _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in - return try manifestMap[dependency.identity].flatMap({ manifest in - - let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: parentTraits) }.map(\.name) -// .map({ EnabledTrait(name: $0.name, setBy: .package(.init(parent))) }) - if let explicitlyEnabledTraits { - let explicitlyEnabledTraits = EnabledTraits( - explicitlyEnabledTraits, - setBy: .package(.init(parent)) - ) - let calculatedTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) - self.enabledTraitsMap[dependency.identity] = calculatedTraits - } -// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { -// let calculatedTraits = try manifest.enabledTraits( -// using: enabledTraitsSet -//// .init(parent) +// public func precomputeTraits( +// _ topLevelManifests: [Manifest], +// _ manifestMap: [PackageIdentity: Manifest] +// ) throws -> [PackageIdentity: EnabledTraits] { +// var visited: Set = [] +// +// func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { +// let parentTraits = self.enabledTraitsMap[parent.packageIdentity] +// let requiredDependencies = try parent.dependenciesRequired(for: productFilter, parentTraits) +// let guardedDependencies = parent.dependenciesTraitGuarded(withEnabledTraits: parentTraits) +// +// _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in +// return try manifestMap[dependency.identity].flatMap({ manifest in +// +// let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: parentTraits) }.map(\.name) +//// .map({ EnabledTrait(name: $0.name, setBy: .package(.init(parent))) }) +// if let explicitlyEnabledTraits { +// let explicitlyEnabledTraits = EnabledTraits( +// explicitlyEnabledTraits, +// setBy: .package(.init(parent)) // ) +// let calculatedTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) // self.enabledTraitsMap[dependency.identity] = calculatedTraits // } - - let result = visited.insert(dependency.identity) - if result.inserted { - try dependencies(of: manifest, dependency.productFilter) - } - - return manifest - }) - }) - } - - for manifest in topLevelManifests { - // Track already-visited manifests to avoid cycles - let result = visited.insert(manifest.packageIdentity) - if result.inserted { - try dependencies(of: manifest) - } - } - - print("enabled traits map: \(enabledTraitsMap)") - return self.enabledTraitsMap.dictionaryLiteral - } +//// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { +//// let calculatedTraits = try manifest.enabledTraits( +//// using: enabledTraitsSet +////// .init(parent) +//// ) +//// self.enabledTraitsMap[dependency.identity] = calculatedTraits +//// } +// +// let result = visited.insert(dependency.identity) +// if result.inserted { +// try dependencies(of: manifest, dependency.productFilter) +// } +// +// return manifest +// }) +// }) +// } +// +// for manifest in topLevelManifests { +// // Track already-visited manifests to avoid cycles +// let result = visited.insert(manifest.packageIdentity) +// if result.inserted { +// try dependencies(of: manifest) +// } +// } +// +// return self.enabledTraitsMap.dictionaryLiteral +// } } diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index 0f7dcdcdc64..76054b6988d 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -1097,8 +1097,8 @@ extension Workspace { ) return (package, manifest) } catch { + // Propagate the TraitError if it exists. if let error = error as? TraitError { - print("trait error; please propagate") throw error } return nil @@ -1111,12 +1111,6 @@ extension Workspace { if let (package, manifest) = result { // Store the manifest. rootManifests[package] = manifest - - // TODO bp: call update enabled traits here instead? - // Compute the enabled traits for roots. -// let traitConfiguration = self.traitConfiguration -// let enabledTraits = try manifest.enabledTraits(using: traitConfiguration) -// self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits } } diff --git a/Sources/_InternalTestSupport/MockWorkspace.swift b/Sources/_InternalTestSupport/MockWorkspace.swift index ffdc3fc309b..4e8b202654f 100644 --- a/Sources/_InternalTestSupport/MockWorkspace.swift +++ b/Sources/_InternalTestSupport/MockWorkspace.swift @@ -624,8 +624,6 @@ public final class MockWorkspace { expectedSigningEntities: expectedSigningEntities, observabilityScope: observability.topScope ) - print("mock enabled traits map: \(self.enabledTraitsMap)") - print("real enabled traits map: \(workspace.enabledTraitsMap)") try result(graph, observability.diagnostics) } catch { // helpful when graph fails to load From d3ae1a923f3ec93a0a7f3953b0c776bf6b0a2861 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 30 Oct 2025 16:28:30 -0400 Subject: [PATCH 07/14] Update EnabledTraitTests --- Tests/PackageModelTests/EnabledTraitTests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/PackageModelTests/EnabledTraitTests.swift b/Tests/PackageModelTests/EnabledTraitTests.swift index d0b83ea9469..7aa57af06d3 100644 --- a/Tests/PackageModelTests/EnabledTraitTests.swift +++ b/Tests/PackageModelTests/EnabledTraitTests.swift @@ -294,6 +294,9 @@ struct EnabledTraitTests { #expect(traits.contains("Chocolate")) let bananaTrait = try traits.unwrapRemove("Banana") + #expect(traits.count == 1) + #expect(traits.contains("Chocolate")) + #expect(!traits.contains(bananaTrait)) } // MARK: - EnabledTraitsMap Tests From ab987e6197c0873e709b25f3e00ec48111a656af Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 30 Oct 2025 16:51:14 -0400 Subject: [PATCH 08/14] Revert breaking change in trait-guarded calculation --- Sources/PackageModel/Manifest/Manifest+Traits.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/PackageModel/Manifest/Manifest+Traits.swift b/Sources/PackageModel/Manifest/Manifest+Traits.swift index 363a4331f64..29f7ebca3ab 100644 --- a/Sources/PackageModel/Manifest/Manifest+Traits.swift +++ b/Sources/PackageModel/Manifest/Manifest+Traits.swift @@ -469,8 +469,12 @@ extension Manifest { // Determine whether the current set of enabled traits still gate the package dependency // across targets. - let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.filter({ $0.condition?.traits != nil }).allSatisfy({ self.isTraitGuarded($0, enabledTraits: enabledTraits) - }) +// let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.filter({ $0.condition?.traits != nil }).allSatisfy({ self.isTraitGuarded($0, enabledTraits: enabledTraits) +// }) + let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({ + let isGuarded = $0.intersection(enabledTraitNames).isEmpty + return isGuarded + }) // Since we only omit a package dependency that is only guarded by traits, determine // whether this dependency is used elsewhere without traits. From 33496a8d5b4b7424e1bdbcd3df453a7396129d61 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Fri, 31 Oct 2025 17:34:33 -0400 Subject: [PATCH 09/14] Fix IdentifiableSet intersection method, separate traits tests, clean EnabledTrait and add descriptions --- .../Basics/Collections/IdentifiableSet.swift | 2 +- Sources/PackageGraph/ModulesGraph.swift | 72 +- Sources/PackageModel/EnabledTrait.swift | 272 +++-- .../Manifest/Manifest+Traits.swift | 9 +- .../MockPackageGraphs.swift | 15 +- .../ModulesGraphTests+Traits.swift | 1053 +++++++++++++++++ .../PackageGraphTests/ModulesGraphTests.swift | 989 ---------------- .../PackageModelTests/EnabledTraitTests.swift | 328 ++++- .../WorkspaceTests+Traits.swift | 13 +- 9 files changed, 1561 insertions(+), 1192 deletions(-) create mode 100644 Tests/PackageGraphTests/ModulesGraphTests+Traits.swift diff --git a/Sources/Basics/Collections/IdentifiableSet.swift b/Sources/Basics/Collections/IdentifiableSet.swift index c60a86868ab..70b80355823 100644 --- a/Sources/Basics/Collections/IdentifiableSet.swift +++ b/Sources/Basics/Collections/IdentifiableSet.swift @@ -83,7 +83,7 @@ public struct IdentifiableSet: Collection { public func intersection(_ otherSequence: some Sequence) -> Self { let keysToRemove = Set(self.storage.keys).subtracting(otherSequence.map(\.id)) - var result = Self() + var result = self for key in keysToRemove { result.storage.removeValue(forKey: key) } diff --git a/Sources/PackageGraph/ModulesGraph.swift b/Sources/PackageGraph/ModulesGraph.swift index 7515c8ebd73..5df55390e6f 100644 --- a/Sources/PackageGraph/ModulesGraph.swift +++ b/Sources/PackageGraph/ModulesGraph.swift @@ -466,7 +466,8 @@ public func loadModulesGraph( useXCBuildFileRules: Bool = false, customXCTestMinimumDeploymentTargets: [PackageModel.Platform: PlatformVersion]? = .none, observabilityScope: ObservabilityScope, - traitConfiguration: TraitConfiguration = .default + traitConfiguration: TraitConfiguration = .default, + enabledTraitsMap: EnabledTraitsMap = .init() ) throws -> ModulesGraph { let rootManifests = manifests.filter(\.packageKind.isRoot).spm_createDictionary { ($0.path, $0) } let externalManifests = try manifests.filter { !$0.packageKind.isRoot } @@ -479,75 +480,6 @@ public func loadModulesGraph( let packages = Array(rootManifests.keys) - let manifestMap = manifests.reduce(into: [PackageIdentity: Manifest]()) { manifestMap, manifest in - manifestMap[manifest.packageIdentity] = manifest - } - - // Note: The following is a copy of the existing `Workspace.precomputeTraits` method - func precomputeTraits( - _ enabledTraitsMap: EnabledTraitsMap, - _ topLevelManifests: [Manifest], - _ manifestMap: [PackageIdentity: Manifest] - ) throws -> [PackageIdentity: EnabledTraits] { - var visited: Set = [] - var enabledTraitsMap = enabledTraitsMap - - func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { - let parentTraits = enabledTraitsMap[parent.packageIdentity] - let requiredDependencies = try parent.dependenciesRequired(for: productFilter, parentTraits) - let guardedDependencies = parent.dependenciesTraitGuarded(withEnabledTraits: parentTraits) - - _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in - return try manifestMap[dependency.identity].flatMap({ manifest in - // TODO bp can this be shortened as an initializer for EnabledTraits? - // possibly have it filter across the dependencies itself - let explicitlyEnabledTraitsSet = dependency.traits?.filter({ $0.isEnabled(by: parentTraits) }).map(\.name) - if let explicitlyEnabledTraitsSet { - let explicitlyEnabledTraits = EnabledTraits( - explicitlyEnabledTraitsSet, - setBy: .package(.init(identity: parent.packageIdentity, name: parent.displayName)) - ) - enabledTraitsMap[dependency.identity] = try manifest.enabledTraits(using: explicitlyEnabledTraits) - } - -// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { -// let calculatedTraits = try manifest.enabledTraits( -// using: enabledTraitsSet -//// .init(parent) -// ) -// enabledTraitsMap[dependency.identity] = calculatedTraits -// } - - let result = visited.insert(dependency.identity) - if result.inserted { - try dependencies(of: manifest, dependency.productFilter) - } - - return manifest - }) - }) - } - - for manifest in topLevelManifests { - // Track already-visited manifests to avoid cycles - let result = visited.insert(manifest.packageIdentity) - if result.inserted { - try dependencies(of: manifest) - } - } - - return enabledTraitsMap.dictionaryLiteral - } - - - // Precompute enabled traits for roots. - var enabledTraitsMap: EnabledTraitsMap = [:] - for root in rootManifests.values { - let enabledTraits = try root.enabledTraits(using: traitConfiguration) - enabledTraitsMap[root.packageIdentity] = enabledTraits - } - enabledTraitsMap = .init(try precomputeTraits(enabledTraitsMap, manifests, manifestMap)) - let input = PackageGraphRootInput(packages: packages, traitConfiguration: traitConfiguration) let graphRoot = try PackageGraphRoot( input: input, diff --git a/Sources/PackageModel/EnabledTrait.swift b/Sources/PackageModel/EnabledTrait.swift index 79e58e4bd0a..d411654eb6f 100644 --- a/Sources/PackageModel/EnabledTrait.swift +++ b/Sources/PackageModel/EnabledTrait.swift @@ -12,29 +12,20 @@ import Basics +// MARK: - EnabledTraitsMap + /// A wrapper struct for a dictionary that stores the transitively enabled traits for each package. /// This struct implicitly omits adding `default` traits to its storage, and returns `nil` if it there is no existing entry for /// a given package, since if there are no explicitly enabled traits set by anything else a package will then default to its `default` traits, /// if they exist. -public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { +public struct EnabledTraitsMap { public typealias Key = PackageIdentity public typealias Value = EnabledTraits - var storage: ThreadSafeKeyValueStore = .init() -// var storage: [PackageIdentity: EnabledTraits] = [:] + private var storage: ThreadSafeKeyValueStore = .init() public init() { } - public init(dictionaryLiteral elements: (Key, Value)...) { - for (key, value) in elements { - storage[key] = value - } - } - - public init(_ dictionary: [Key: Value]) { - self.storage = .init(dictionary) - } - public subscript(key: PackageIdentity) -> EnabledTraits { get { storage[key] ?? ["default"] } set { @@ -51,70 +42,54 @@ public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { } } + /// Returns a list of traits that were explicitly enabled for a given package. public subscript(explicitlyEnabledTraitsFor key: PackageIdentity) -> EnabledTraits? { get { storage[key] } } + /// Returns a dictionary literal representation of the map. public var dictionaryLiteral: [PackageIdentity: EnabledTraits] { return storage.get() } } +// MARK: EnabledTraitsMap + ExpressibleByDictionaryLiteral +extension EnabledTraitsMap: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (Key, Value)...) { + for (key, value) in elements { + storage[key] = value + } + } + + public init(_ dictionary: [Key: Value]) { + self.storage = .init(dictionary) + } +} + // MARK: - EnabledTrait /// A structure representing a trait that is enabled. The package in which this is enabled on is identified in /// the EnabledTraitsMap. /// /// An enabled trait is a trait that is either explicitly enabled by a user-passed trait configuration from the command line, -/// a parent package that has defined enabled traits for its dependency package(s), or by another trait (including the default case). +/// a parent package that has defined enabled traits for its dependency package, or transitively by another trait (including the default case). /// -/// An EnabledTrait is differentiated by its `name`, and all other data stored in this struct is treated as metadata for -/// convenience. When unifying two `EnabledTrait`s, it will combine the list of setters (`setBy`) if the `name`s match. +/// An `EnabledTrait` is differentiated by its `name`, and all other data stored in this struct is treated as metadata for +/// convenience. When unifying two `EnabledTrait`s, it will combine the list of setters if the `name`s match. /// -public struct EnabledTrait: Identifiable, CustomStringConvertible, ExpressibleByStringLiteral, Comparable { - // Convenience typealias for a list of `Setter` +public struct EnabledTrait: Identifiable { + /// Convenience typealias for a list of `Setter` public typealias Setters = Set - // The identifier for the trait. + /// The identifier for the trait. public var id: String { name } - // The name of the trait. + /// The name of the trait. public let name: String - // The list of setters who have enabled this trait. + /// The list of setters who have enabled this trait. public var setters: Setters = [] - /// An enumeration that describes where a given trait was enabled. - public enum Setter: Hashable, CustomStringConvertible { - case traitConfiguration - case package(Manifest.PackageIdentifier) - case trait(String) - - public var description: String { - switch self { - case .traitConfiguration: - "custom trait configuration." - case .package(let parent): - parent.description - case .trait(let trait): - trait - } - } - - public var parentPackage: Manifest.PackageIdentifier? { - switch self { - case .package(let id): - return id - case .traitConfiguration, .trait: - return nil - } - } - - public static var `default`: Self { - .trait("default") - } - } - public init(name: String, setBy: Setter) { self.name = name self.setters = [setBy] @@ -125,6 +100,7 @@ public struct EnabledTrait: Identifiable, CustomStringConvertible, ExpressibleBy self.setters = Set(setBy) } + /// The packages that have enabled this trait. public var parentPackages: [Manifest.PackageIdentifier] { setters.compactMap(\.parentPackage) } @@ -133,6 +109,9 @@ public struct EnabledTrait: Identifiable, CustomStringConvertible, ExpressibleBy name == "default" } + /// Returns a new `EnabledTrait` that contains a merged list of `Setters` from + /// `self` and the `otherTrait`, only if the traits are equal. Otherwise, returns nil. + /// - Parameter otherTrait: The trait to merge in. public func unify(_ otherTrait: EnabledTrait) -> EnabledTrait? { guard self.name == otherTrait.name else { return nil @@ -142,19 +121,47 @@ public struct EnabledTrait: Identifiable, CustomStringConvertible, ExpressibleBy updatedTrait.setters = setters.union(otherTrait.setters) return updatedTrait } +} - // MARK: - CustomStringConvertible - public var description: String { - name - } +// MARK: EnabledTrait.Setter - // MARK: - ExpressibleByStringLiteral - public init(stringLiteral value: String) { - self.name = value +extension EnabledTrait { + /// An enumeration that describes how a given trait was set as enabled. + public enum Setter: Hashable, CustomStringConvertible { + case traitConfiguration + case package(Manifest.PackageIdentifier) + case trait(String) + + public var description: String { + switch self { + case .traitConfiguration: + "command-line trait configuration" + case .package(let parent): + "parent package: \(parent.description)" + case .trait(let trait): + "trait: \(trait)" + } + } + + /// The identifier of the parent package that defined this trait, if any. + public var parentPackage: Manifest.PackageIdentifier? { + switch self { + case .package(let id): + return id + case .traitConfiguration, .trait: + return nil + } + } + + public static var `default`: Self { + .trait("default") + } } +} - // MARK: - Equatable +// MARK: EnabledTrait + Equatable +extension EnabledTrait: Equatable { // When comparing two `EnabledTraits`, if the names are the same then // we know that these two objects are referring to the same trait of a package. // In this case, the two objects should be combined into one. @@ -169,18 +176,40 @@ public struct EnabledTrait: Identifiable, CustomStringConvertible, ExpressibleBy public static func ==(lhs: String, rhs: EnabledTrait) -> Bool { return lhs == rhs.name } +} - // MARK: - Comparable +// MARK: EnabledTrait + Comparable +extension EnabledTrait: Comparable { public static func <(lhs: EnabledTrait, rhs: EnabledTrait) -> Bool { return lhs.name < rhs.name } } +// MARK: EnabledTrait + CustomStringConvertible + +extension EnabledTrait: CustomStringConvertible { + public var description: String { + name + } +} + +// MARK: EnabledTrait + ExpressibleByStringLiteral + +extension EnabledTrait: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.name = value + } +} + // MARK: - EnabledTraits -/// This struct acts as a wrapper for a set of `EnabledTrait` to handle special cases. -public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { +/// A collection wrapper around a set of `EnabledTrait` instances that provides specialized behavior +/// for trait management. This struct ensures that traits with the same name are automatically unified +/// by merging their setters when inserted, maintaining a single entry per unique trait name. It provides +/// convenient set operations like union and intersection, along with collection protocol conformance for +/// easy iteration and manipulation of enabled traits. +public struct EnabledTraits: Hashable { public typealias Element = EnabledTrait public typealias Index = IdentifiableSet.Index @@ -190,12 +219,6 @@ public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { ["default"] } - public init(arrayLiteral elements: Element...) { - for element in elements { - _traits.insert(element) - } - } - public init(_ traits: C, setBy origin: EnabledTrait.Setter) where C.Element == String { let enabledTraits = traits.map({ EnabledTrait(name: $0, setBy: origin) }) self.init(enabledTraits) @@ -205,6 +228,24 @@ public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { self._traits = IdentifiableSet(traits) } + public static func ==(_ lhs: EnabledTraits, _ rhs: EnabledTraits) -> Bool { + lhs._traits.names == rhs._traits.names + } +} + +// MARK: EnabledTraits + ExpressibleByArrayLiteral + +extension EnabledTraits: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: Element...) { + for element in elements { + _traits.insert(element) + } + } +} + +// MARK: EnabledTraits + Collection + +extension EnabledTraits: Collection { public var startIndex: Index { return _traits.startIndex } @@ -221,23 +262,20 @@ public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { return _traits[position] } - public mutating func formUnion(_ other: EnabledTraits) { - self._traits = self.union(other)._traits + public mutating func insert(_ newMember: Element) { + _traits.insert(newMember) } - public func flatMap(_ transform: (Self.Element) throws -> EnabledTraits) rethrows -> EnabledTraits { - let transformedTraits = try _traits.flatMap(transform) - return EnabledTraits(transformedTraits) + public mutating func remove(_ member: Element) -> Element? { + return _traits.remove(member) } - public func map(_ transform: (Self.Element) throws -> Self.Element) rethrows -> EnabledTraits { - let transformedTraits = try _traits.map(transform) - return EnabledTraits(transformedTraits) + public func contains(_ member: Element) -> Bool { + return _traits.contains(member) } - public func union(_ other: EnabledTraits) -> EnabledTraits { - let unionedTraits = _traits.union(other) - return EnabledTraits(unionedTraits) + public func intersection(_ other: C) -> EnabledTraits where C.Element == String { + self.intersection(other.map(\.asEnabledTrait)) } public func intersection(_ other: C) -> EnabledTraits where C.Element == Self.Element { @@ -246,20 +284,23 @@ public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { return EnabledTraits(intersection) } - public func intersection(_ other: C) -> EnabledTraits where C.Element == String { - self.intersection(other.map(\.asEnabledTrait)) + public func union(_ other: EnabledTraits) -> EnabledTraits { + let unionedTraits = _traits.union(other) + return EnabledTraits(unionedTraits) } - public mutating func remove(_ member: Element) -> Element? { - return _traits.remove(member) + public mutating func formUnion(_ other: EnabledTraits) { + self._traits = self.union(other)._traits } - public mutating func insert(_ newMember: Element) { - _traits.insert(newMember) + public func map(_ transform: (Self.Element) throws -> Self.Element) rethrows -> EnabledTraits { + let transformedTraits = try _traits.map(transform) + return EnabledTraits(transformedTraits) } - public func contains(_ member: Element) -> Bool { - return _traits.contains(member) + public func flatMap(_ transform: (Self.Element) throws -> EnabledTraits) rethrows -> EnabledTraits { + let transformedTraits = try _traits.flatMap(transform) + return EnabledTraits(transformedTraits) } public static func ==(_ lhs: EnabledTraits, _ rhs: C) -> Bool where C.Element == Element { @@ -269,13 +310,32 @@ public struct EnabledTraits: ExpressibleByArrayLiteral, Collection, Hashable { public static func ==(_ lhs: C, _ rhs: EnabledTraits) -> Bool where C.Element == Element { lhs.names == rhs._traits.names } +} - public static func ==(_ lhs: EnabledTraits, _ rhs: EnabledTraits) -> Bool { - lhs._traits.names == rhs._traits.names +// MARK: - EnabledTraitConvertible + +/// Represents a type that can be converted into an `EnabledTrait`. +/// This protocol enables conversion between string-like types and `EnabledTrait` instances, +/// allowing for more flexible APIs that can accept either strings or traits interchangeably. +package protocol EnabledTraitConvertible: Equatable { + var asEnabledTrait: EnabledTrait { get } +} + +// MARK: String + EnabledTraitConvertible + +extension String: EnabledTraitConvertible { + package var asEnabledTrait: EnabledTrait { + .init(stringLiteral: self) } } +// MARK: - Collection + EnabledTrait + extension Collection where Element == EnabledTrait { + public var names: Set { + Set(self.map(\.name)) + } + public func contains(_ trait: String) -> Bool { return self.map(\.name).contains(trait) } @@ -284,16 +344,23 @@ extension Collection where Element == EnabledTrait { return self.contains(trait.description) } - public var names: Set { - Set(self.map(\.name)) - } - public func joined(separator: String = "") -> String { names.joined(separator: separator) } } + +// MARK: - IdentifiableSet + EnabledTrait + extension IdentifiableSet where Element == EnabledTrait { + private mutating func insertTrait(_ member: Element) { + if let oldElement = self.remove(member), let newElement = oldElement.unify(member) { + insert(newElement) + } else { + insert(member) + } + } + public func union(_ other: IdentifiableSet) -> IdentifiableSet { var updatedContents = self for element in other { @@ -309,22 +376,5 @@ extension IdentifiableSet where Element == EnabledTrait { return self.union(IdentifiableSet(other.map({ $0 }))) } } - - private mutating func insertTrait(_ member: Element) { - if let oldElement = self.remove(member), let newElement = oldElement.unify(member) { - insert(newElement) - } else { - insert(member) - } - } -} - -package protocol EnabledTraitConvertible { - var asEnabledTrait: EnabledTrait { get } } -extension String: EnabledTraitConvertible { - package var asEnabledTrait: EnabledTrait { - .init(stringLiteral: self) - } -} diff --git a/Sources/PackageModel/Manifest/Manifest+Traits.swift b/Sources/PackageModel/Manifest/Manifest+Traits.swift index 29f7ebca3ab..6d5fb017502 100644 --- a/Sources/PackageModel/Manifest/Manifest+Traits.swift +++ b/Sources/PackageModel/Manifest/Manifest+Traits.swift @@ -469,12 +469,8 @@ extension Manifest { // Determine whether the current set of enabled traits still gate the package dependency // across targets. -// let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.filter({ $0.condition?.traits != nil }).allSatisfy({ self.isTraitGuarded($0, enabledTraits: enabledTraits) -// }) - let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({ - let isGuarded = $0.intersection(enabledTraitNames).isEmpty - return isGuarded - }) + let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.filter({ $0.condition?.traits != nil }).allSatisfy({ self.isTraitGuarded($0, enabledTraits: enabledTraits) + }) // Since we only omit a package dependency that is only guarded by traits, determine // whether this dependency is used elsewhere without traits. @@ -488,7 +484,6 @@ extension Manifest { enabledTraits: EnabledTraits ) -> Bool { // Validate enabled traits -// try validateEnabledTraits(enabledTraits) guard let condition = dependency.condition, let traits = condition.traits else { return false } return enabledTraits.intersection(traits).isEmpty diff --git a/Sources/_InternalTestSupport/MockPackageGraphs.swift b/Sources/_InternalTestSupport/MockPackageGraphs.swift index 7e60fa7c41d..1516d517a1a 100644 --- a/Sources/_InternalTestSupport/MockPackageGraphs.swift +++ b/Sources/_InternalTestSupport/MockPackageGraphs.swift @@ -126,7 +126,8 @@ package func macrosPackageGraph() throws -> MockPackageGraph { ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) @@ -284,7 +285,8 @@ package func macrosTestsPackageGraph() throws -> MockPackageGraph { ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) @@ -317,7 +319,8 @@ package func trivialPackageGraph() throws -> MockPackageGraph { ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) @@ -361,7 +364,8 @@ package func embeddedCxxInteropPackageGraph() throws -> MockPackageGraph { ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) @@ -426,7 +430,8 @@ package func toolsExplicitLibrariesGraph(linkage: ProductType.LibraryType) throw ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) diff --git a/Tests/PackageGraphTests/ModulesGraphTests+Traits.swift b/Tests/PackageGraphTests/ModulesGraphTests+Traits.swift new file mode 100644 index 00000000000..4212ae76612 --- /dev/null +++ b/Tests/PackageGraphTests/ModulesGraphTests+Traits.swift @@ -0,0 +1,1053 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import PackageModel +import TSCUtility +import Testing +import _InternalTestSupport + +@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) +@testable import PackageGraph + +extension ModulesGraphTests { + @Test + func traits_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: "default", enabledTraits: ["Trait1"]), + "Trait1", + ] + ), + ], + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Foo": ["Trait1"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Foo") { package in + #expect(package.enabledTraits == ["Trait1"]) + } + } + } + + @Test + func traits_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: "default", enabledTraits: ["Trait1"]), + .init(name: "Trait1", enabledTraits: ["Trait2"]), + .init(name: "Trait2", enabledTraits: ["Trait3", "Trait4"]), + "Trait3", + .init(name: "Trait4", enabledTraits: ["Trait5"]), + "Trait5", + ] + ), + ], + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Foo": ["Trait1", "Trait2", "Trait3", "Trait4", "Trait5"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Foo") { package in + #expect(package.enabledTraits == ["Trait1", "Trait2", "Trait3", "Trait4", "Trait5"]) + } + } + } + + @Test + func traits_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: "default", enabledTraits: ["Package1Trait1"]), + "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, + enabledTraitsMap: [ + "Package1": ["Package1Trait1"], + "Package2": ["Package2Trait1"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait1"]) + #expect(package.dependencies.count == 1) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == ["Package2Trait1"]) + } + } + } + + @Test + func traits_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: "default", enabledTraits: ["Package1Trait1"]), + .init(name: "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", + ] + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Package1": ["Package1Trait1"], + "Package2": ["Package2Trait1"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait1"]) + #expect(package.dependencies.count == 1) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == ["Package2Trait1"]) + } + } + } + + @Test + func traits_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: "default", enabledTraits: ["Package1Trait1", "Package1Trait2"]), + .init(name: "Package1Trait1"), + .init(name: "Package1Trait2"), + ] + ), + 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, + enabledTraitsMap: [ + "Package1": ["Package1Trait1", "Package1Trait2"], + "Package2": ["Package2Trait1"], + "Package3": ["Package3Trait1"], + "Package4": ["Package4Trait1", "Package4Trait2"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait1", "Package1Trait2"]) + #expect(package.dependencies.count == 3) + } + try 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"]), + ] + ) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == ["Package2Trait1"]) + } + try result.checkPackage("Package3") { package in + #expect(package.enabledTraits == ["Package3Trait1"]) + } + try result.checkPackage("Package4") { package in + #expect(package.enabledTraits == ["Package4Trait1", "Package4Trait2"]) + } + } + } + + @Test + func traits_whenPruneDependenciesEnabled() 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"]) + ), + .init( + tool: .swift, + kind: .define("TEST_DEFINE_2"), + condition: .init(traits: ["Package1Trait3"]) + ), + ] + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Package1Trait3"]), + .init(name: "Package1Trait1"), + .init(name: "Package1Trait2"), + .init(name: "Package1Trait3"), + ], + pruneDependencies: 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", + ], + pruneDependencies: true + ), + 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", + ], + pruneDependencies: true + ), + 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", + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package5", + path: "/Package5", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package5Target1", + type: .library(.automatic), + targets: ["Package5Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package5Target1" + ), + ], + pruneDependencies: true + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Package1": ["Package1Trait3"], + "Package2": [], + "Package3": [], + "Package4": ["Package4Trait2"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait3"]) + #expect(package.dependencies.count == 2) + } + try result.checkTarget("Package1Target1") { target in + target.check(dependencies: "Package2Target1", "Package4Target1") + target.checkBuildSetting( + declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, + assignments: [ + .init(values: ["TEST_DEFINE_2"], conditions: [.traits(.init(traits: ["Package1Trait3"]))]), + .init(values: ["Package1Trait3"]), + ] + ) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == []) + } + try result.checkPackage("Package3") { package in + #expect(package.enabledTraits == []) + } + try result.checkPackage("Package4") { package in + #expect(package.enabledTraits == ["Package4Trait2"]) + } + } + } + + @Test + func traits_whenPruneDependenciesEnabledForSomeManifests() 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"]) + ), + .init( + tool: .swift, + kind: .define("TEST_DEFINE_2"), + condition: .init(traits: ["Package1Trait3"]) + ), + ] + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Package1Trait3"]), + .init(name: "Package1Trait1"), + .init(name: "Package1Trait2"), + .init(name: "Package1Trait3"), + ], + pruneDependencies: false + ), + 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", + ], + pruneDependencies: true + ), + 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", + ], + pruneDependencies: true + ), + 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", + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package5", + path: "/Package5", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package5Target1", + type: .library(.automatic), + targets: ["Package5Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package5Target1" + ), + ], + pruneDependencies: true + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Package1": ["Package1Trait3"], + "Package2": [], + "Package3": [], + "Package4": ["Package4Trait2"] + ] + ) + + #expect(observability.diagnostics.count == 0) + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait3"]) + #expect(package.dependencies.count == 2) + } + try result.checkTarget("Package1Target1") { target in + target.check(dependencies: "Package2Target1", "Package4Target1") + target.checkBuildSetting( + declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, + assignments: [ + .init(values: ["TEST_DEFINE_2"], conditions: [.traits(.init(traits: ["Package1Trait3"]))]), + .init(values: ["Package1Trait3"]), + ] + ) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == []) + } + try result.checkPackage("Package3") { package in + #expect(package.enabledTraits == []) + } + try result.checkPackage("Package4") { package in + #expect(package.enabledTraits == ["Package4Trait2"]) + } + } + } + + @Test + func traits_whenConditionalDependencies() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Lunch/Sources/Drink/source.swift", + "/Caffeine/Sources/CoffeeTarget/source.swift", + "/Juice/Sources/AppleJuiceTarget/source.swift", + ) + + let manifests = try [ + Manifest.createRootManifest( + displayName: "Lunch", + path: "/Lunch", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Caffeine", + requirement: .upToNextMajor(from: "1.0.0"), + ), + .localSourceControl( + path: "/Juice", + requirement: .upToNextMajor(from: "1.0.0") + ) + ], + targets: [ + TargetDescription( + name: "Drink", + dependencies: [ + .product( + name: "Coffee", + package: "Caffeine", + condition: .init(traits: ["EnableCoffeeDep"]) + ), + .product( + name: "AppleJuice", + package: "Juice", + condition: .init(traits: ["EnableAppleJuiceDep"]) + ) + ], + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["EnableCoffeeDep"]), + .init(name: "EnableCoffeeDep"), + .init(name: "EnableAppleJuiceDep"), + ], + ), + Manifest.createFileSystemManifest( + displayName: "Caffeine", + path: "/Caffeine", + toolsVersion: .v5_9, + products: [ + .init( + name: "Coffee", + type: .library(.automatic), + targets: ["CoffeeTarget"] + ), + ], + targets: [ + TargetDescription( + name: "CoffeeTarget", + ), + ], + ), + Manifest.createFileSystemManifest( + displayName: "Juice", + path: "/Juice", + toolsVersion: .v5_9, + products: [ + .init( + name: "AppleJuice", + type: .library(.automatic), + targets: ["AppleJuiceTarget"] + ), + ], + targets: [ + TargetDescription( + name: "AppleJuiceTarget", + ), + ], + ) + ] + + // Test graph with default trait configuration + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Lunch": ["EnableCoffeeDep"] + ] + ) + + #expect(observability.diagnostics.count == 0) + try PackageGraphTester(graph) { result in + try result.checkPackage("Lunch") { package in + #expect(package.enabledTraits == ["EnableCoffeeDep"]) + #expect(package.dependencies.count == 1) + } + try result.checkTarget("Drink") { target in + target.check(dependencies: "Coffee") + } + try result.checkPackage("Caffeine") { package in + #expect(package.enabledTraits == ["default"]) + } + try result.checkPackage("Juice") { package in + #expect(package.enabledTraits == ["default"]) + } + } + + // Test graph when disabling all traits + let graphWithTraitsDisabled = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + traitConfiguration: .disableAllTraits, + enabledTraitsMap: [ + "Lunch": [], + ] + ) + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graphWithTraitsDisabled) { result in + try result.checkPackage("Lunch") { package in + #expect(package.enabledTraits == []) + #expect(package.dependencies.count == 0) + } + try result.checkTarget("Drink") { target in + #expect(target.target.dependencies.isEmpty) + } + try result.checkPackage("Caffeine") { package in + #expect(package.enabledTraits == ["default"]) + } + try result.checkPackage("Juice") { package in + #expect(package.enabledTraits == ["default"]) + } + } + + // Test graph when we set a trait configuration that enables different traits than the defaults + let graphWithDifferentEnabledTraits = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + traitConfiguration: .enabledTraits(["EnableAppleJuiceDep"]), + enabledTraitsMap: [ + "Lunch": ["EnableAppleJuiceDep"], + ] + ) + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graphWithDifferentEnabledTraits) { result in + try result.checkPackage("Lunch") { package in + #expect(package.enabledTraits == ["EnableAppleJuiceDep"]) + #expect(package.dependencies.count == 1) + } + try result.checkTarget("Drink") { target in + target.check(dependencies: "AppleJuice") + } + try result.checkPackage("Caffeine") { package in + #expect(package.enabledTraits == ["default"]) + } + try result.checkPackage("Juice") { package in + #expect(package.enabledTraits == ["default"]) + } + } + } + +} diff --git a/Tests/PackageGraphTests/ModulesGraphTests.swift b/Tests/PackageGraphTests/ModulesGraphTests.swift index 7145a61ea3d..1089a3ab30b 100644 --- a/Tests/PackageGraphTests/ModulesGraphTests.swift +++ b/Tests/PackageGraphTests/ModulesGraphTests.swift @@ -3792,995 +3792,6 @@ struct ModulesGraphTests { ) } } - - @Test - func traits_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: "default", enabledTraits: ["Trait1"]), - "Trait1", - ] - ), - ], - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Foo") { package in - #expect(package.enabledTraits == ["Trait1"]) - } - } - } - - @Test - func traits_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: "default", enabledTraits: ["Trait1"]), - .init(name: "Trait1", enabledTraits: ["Trait2"]), - .init(name: "Trait2", enabledTraits: ["Trait3", "Trait4"]), - "Trait3", - .init(name: "Trait4", enabledTraits: ["Trait5"]), - "Trait5", - ] - ), - ], - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Foo") { package in - #expect(package.enabledTraits == ["Trait1", "Trait2", "Trait3", "Trait4", "Trait5"]) - } - } - } - - @Test - func traits_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: "default", enabledTraits: ["Package1Trait1"]), - "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 - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait1"]) - #expect(package.dependencies.count == 1) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == ["Package2Trait1"]) - } - } - } - - @Test - func traits_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: "default", enabledTraits: ["Package1Trait1"]), - .init(name: "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", - ] - ), - ] - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait1"]) - #expect(package.dependencies.count == 1) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == ["Package2Trait1"]) - } - } - } - - @Test - func traits_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: "default", enabledTraits: ["Package1Trait1", "Package1Trait2"]), - .init(name: "Package1Trait1"), - .init(name: "Package1Trait2"), - ] - ), - 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 - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait1", "Package1Trait2"]) - #expect(package.dependencies.count == 3) - } - try 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"]), - ] - ) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == ["Package2Trait1"]) - } - try result.checkPackage("Package3") { package in - #expect(package.enabledTraits == ["Package3Trait1"]) - } - try result.checkPackage("Package4") { package in - #expect(package.enabledTraits == ["Package4Trait1", "Package4Trait2"]) - } - } - } - - @Test - func traits_whenPruneDependenciesEnabled() 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"]) - ), - .init( - tool: .swift, - kind: .define("TEST_DEFINE_2"), - condition: .init(traits: ["Package1Trait3"]) - ), - ] - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Package1Trait3"]), - .init(name: "Package1Trait1"), - .init(name: "Package1Trait2"), - .init(name: "Package1Trait3"), - ], - pruneDependencies: 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", - ], - pruneDependencies: true - ), - 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", - ], - pruneDependencies: true - ), - 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", - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package5", - path: "/Package5", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package5Target1", - type: .library(.automatic), - targets: ["Package5Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package5Target1" - ), - ], - pruneDependencies: true - ), - ] - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait3"]) - #expect(package.dependencies.count == 2) - } - try result.checkTarget("Package1Target1") { target in - target.check(dependencies: "Package2Target1", "Package4Target1") - target.checkBuildSetting( - declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, - assignments: [ - .init(values: ["TEST_DEFINE_2"], conditions: [.traits(.init(traits: ["Package1Trait3"]))]), - .init(values: ["Package1Trait3"]), - ] - ) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == []) - } - try result.checkPackage("Package3") { package in - #expect(package.enabledTraits == []) - } - try result.checkPackage("Package4") { package in - #expect(package.enabledTraits == ["Package4Trait2"]) - } - } - } - - @Test - func traits_whenPruneDependenciesEnabledForSomeManifests() 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"]) - ), - .init( - tool: .swift, - kind: .define("TEST_DEFINE_2"), - condition: .init(traits: ["Package1Trait3"]) - ), - ] - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Package1Trait3"]), - .init(name: "Package1Trait1"), - .init(name: "Package1Trait2"), - .init(name: "Package1Trait3"), - ], - pruneDependencies: false - ), - 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", - ], - pruneDependencies: true - ), - 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", - ], - pruneDependencies: true - ), - 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", - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package5", - path: "/Package5", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package5Target1", - type: .library(.automatic), - targets: ["Package5Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package5Target1" - ), - ], - pruneDependencies: true - ), - ] - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait3"]) - #expect(package.dependencies.count == 2) - } - try result.checkTarget("Package1Target1") { target in - target.check(dependencies: "Package2Target1", "Package4Target1") - target.checkBuildSetting( - declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, - assignments: [ - .init(values: ["TEST_DEFINE_2"], conditions: [.traits(.init(traits: ["Package1Trait3"]))]), - .init(values: ["Package1Trait3"]), - ] - ) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == []) - } - try result.checkPackage("Package3") { package in - #expect(package.enabledTraits == []) - } - try result.checkPackage("Package4") { package in - #expect(package.enabledTraits == ["Package4Trait2"]) - } - } - } - - @Test - func traits_whenConditionalDependencies() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Lunch/Sources/Drink/source.swift", - "/Caffeine/Sources/CoffeeTarget/source.swift", - "/Juice/Sources/AppleJuiceTarget/source.swift", - ) - - let manifests = try [ - Manifest.createRootManifest( - displayName: "Lunch", - path: "/Lunch", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Caffeine", - requirement: .upToNextMajor(from: "1.0.0"), - ), - .localSourceControl( - path: "/Juice", - requirement: .upToNextMajor(from: "1.0.0") - ) - ], - targets: [ - TargetDescription( - name: "Drink", - dependencies: [ - .product( - name: "Coffee", - package: "Caffeine", - condition: .init(traits: ["EnableCoffeeDep"]) - ), - .product( - name: "AppleJuice", - package: "Juice", - condition: .init(traits: ["EnableAppleJuiceDep"]) - ) - ], - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["EnableCoffeeDep"]), - .init(name: "EnableCoffeeDep"), - .init(name: "EnableAppleJuiceDep"), - ], - ), - Manifest.createFileSystemManifest( - displayName: "Caffeine", - path: "/Caffeine", - toolsVersion: .v5_9, - products: [ - .init( - name: "Coffee", - type: .library(.automatic), - targets: ["CoffeeTarget"] - ), - ], - targets: [ - TargetDescription( - name: "CoffeeTarget", - ), - ], - ), - Manifest.createFileSystemManifest( - displayName: "Juice", - path: "/Juice", - toolsVersion: .v5_9, - products: [ - .init( - name: "AppleJuice", - type: .library(.automatic), - targets: ["AppleJuiceTarget"] - ), - ], - targets: [ - TargetDescription( - name: "AppleJuiceTarget", - ), - ], - ) - ] - - // Test graph with default trait configuration - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - try PackageGraphTester(graph) { result in - try result.checkPackage("Lunch") { package in - #expect(package.enabledTraits == ["EnableCoffeeDep"]) - #expect(package.dependencies.count == 1) - } - try result.checkTarget("Drink") { target in - target.check(dependencies: "Coffee") - } - try result.checkPackage("Caffeine") { package in - #expect(package.enabledTraits == ["default"]) - } - try result.checkPackage("Juice") { package in - #expect(package.enabledTraits == ["default"]) - } - } - - // Test graph when disabling all traits - let graphWithTraitsDisabled = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope, - traitConfiguration: .disableAllTraits - ) - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graphWithTraitsDisabled) { result in - try result.checkPackage("Lunch") { package in - #expect(package.enabledTraits == []) - #expect(package.dependencies.count == 0) - } - try result.checkTarget("Drink") { target in - #expect(target.target.dependencies.isEmpty) - } - try result.checkPackage("Caffeine") { package in - #expect(package.enabledTraits == ["default"]) - } - try result.checkPackage("Juice") { package in - #expect(package.enabledTraits == ["default"]) - } - } - - // Test graph when we set a trait configuration that enables different traits than the defaults - let graphWithDifferentEnabledTraits = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope, - traitConfiguration: .enabledTraits(["EnableAppleJuiceDep"]) - ) - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graphWithDifferentEnabledTraits) { result in - try result.checkPackage("Lunch") { package in - #expect(package.enabledTraits == ["EnableAppleJuiceDep"]) - #expect(package.dependencies.count == 1) - } - try result.checkTarget("Drink") { target in - target.check(dependencies: "AppleJuice") - } - try result.checkPackage("Caffeine") { package in - #expect(package.enabledTraits == ["default"]) - } - try result.checkPackage("Juice") { package in - #expect(package.enabledTraits == ["default"]) - } - } - } } extension Manifest { diff --git a/Tests/PackageModelTests/EnabledTraitTests.swift b/Tests/PackageModelTests/EnabledTraitTests.swift index 7aa57af06d3..47d52ab200e 100644 --- a/Tests/PackageModelTests/EnabledTraitTests.swift +++ b/Tests/PackageModelTests/EnabledTraitTests.swift @@ -13,15 +13,17 @@ import Testing import struct PackageModel.EnabledTrait import struct PackageModel.EnabledTraits +import struct PackageModel.EnabledTraitsMap import struct PackageModel.PackageIdentity import class PackageModel.Manifest -@Suite( - -) +@Suite struct EnabledTraitTests { // MARK: - EnabledTrait Tests + + /// Verifies that `EnabledTrait` equality is based solely on the trait name, not the setter. + /// Two traits with the same name but different setters are equal, while traits with different names are not equal. @Test func enabledTrait_checkEquality() { let appleTraitSetByApplePie = EnabledTrait.init(name: "Apple", setBy: .package(.init(identity: "ApplePie"))) @@ -33,6 +35,8 @@ struct EnabledTraitTests { #expect(appleCoreTrait != appleTraitSetByAppleJuice) } + /// Tests that unifying two `EnabledTrait` instances with the same name merges their setters + /// into a single set containing both original setters. @Test func enabledTrait_unifyEqualTraits() throws { let bananaTraitSetByFruit = EnabledTrait(name: "Banana", setBy: .package(.init(identity: "Fruit"))) @@ -47,6 +51,8 @@ struct EnabledTraitTests { #expect(unifiedBananaTrait.setters == setters) } + /// Verifies that attempting to unify two traits with different names returns `nil`, + /// as they cannot be unified. @Test func enabledTrait_unifyDifferentTraits() { let bananaTrait = EnabledTrait(name: "Banana", setBy: .package(.init(identity: "Fruit"))) @@ -58,6 +64,8 @@ struct EnabledTraitTests { #expect(bananaTrait.setters == appleTrait.setters) } + /// Tests that `EnabledTrait` can be compared to a string literal for equality in both + /// directions (trait == string and string == trait). @Test func enabledTrait_compareToStringLiteral() { let appleTrait = EnabledTrait(name: "Apple", setBy: .default) @@ -66,6 +74,19 @@ struct EnabledTraitTests { #expect(appleTrait == "Apple") // test when EnabledTrait lhs } + /// Tests that `EnabledTrait` can be compared to a `String` for equality in both + /// directions (trait == string and string == trait). + @Test + func enabledTrait_compareToStringAsEnabledTraitConvertible() { + let appleTrait = EnabledTrait(name: "Apple", setBy: .default) + let stringTrait = "Apple" + + #expect(stringTrait.asEnabledTrait == appleTrait) // test when EnabledTrait rhs + #expect(appleTrait == stringTrait.asEnabledTrait) // test when EnabledTrait lhs + } + + /// Verifies that an `EnabledTrait` can be initialized using a string literal and is + /// equivalent to initialization with `setBy: .default`. @Test func enabledTrait_initializedByStringLiteral() { let appleTraitByString: EnabledTrait = "Apple" @@ -74,6 +95,7 @@ struct EnabledTraitTests { #expect(appleTraitByString == appleTraitByInit) } + /// Confirms that the `id` property of an `EnabledTrait` equals its `name` property. @Test func enabledTrait_assertIdIsName() { let appleTrait = EnabledTrait(name: "Apple", setBy: .default) @@ -81,6 +103,8 @@ struct EnabledTraitTests { #expect(appleTrait.id == appleTrait.name) } + /// Tests the `isDefault` property to verify that a trait named "default" is correctly + /// identified as a default trait. @Test func enabledTrait_CheckIfDefault() { let defaultTrait: EnabledTrait = "default" @@ -88,6 +112,8 @@ struct EnabledTraitTests { #expect(defaultTrait.isDefault) } + /// Verifies that `EnabledTrait` instances can be sorted alphabetically by name and + /// compared using comparison operators (`<`, `>`). @Test func enabledTrait_SortAndCompare() { let appleTrait: EnabledTrait = "Apple" @@ -103,6 +129,8 @@ struct EnabledTraitTests { #expect(orangeTrait > bananaTrait) } + /// Tests the `parentPackages` property to ensure it correctly filters and returns only + /// package-based setters, excluding trait and trait configuration setters. @Test func enabledTrait_getParentPackageSetters() throws { let traitSetByPackages = EnabledTrait( @@ -127,6 +155,9 @@ struct EnabledTraitTests { // MARK: - EnabledTraits Tests + /// Verifies that `EnabledTraits` can be initialized from an array literal of strings, + /// comparing it against another set of traits initialized by a list of `EnabledTrait` + /// containing traits of the same names. @Test func enabledTraits_initWithStrings() { let enabledTraits: EnabledTraits = ["One", "Two", "Three"] @@ -139,6 +170,8 @@ struct EnabledTraitTests { #expect(enabledTraits == toTestAgainst) } + /// Tests the `.defaults` static property returns an `EnabledTraits` set containing + /// only the "default" trait. @Test func enabledTraits_defaultSet() { let defaults: EnabledTraits = .defaults @@ -147,20 +180,27 @@ struct EnabledTraitTests { #expect(defaults == [EnabledTrait(name: "default", setBy: .default)]) } + /// Verifies the `contains` method works with both string literals and `EnabledTrait` instances, + /// and correctly identifies traits that are and aren't in the set. @Test func enabledTraits_containsTrait() { let enabledTraits: EnabledTraits = ["Apple", "Banana"] // Test against a string literal #expect(enabledTraits.contains("Apple")) + // Test against an explicitly initialized EnabledTrait #expect(enabledTraits.contains(EnabledTrait(name: "Apple", setBy: .default))) + // Test against string literal that is not in the set #expect(!enabledTraits.contains("Orange")) + // Test against initialized EnabledTrait that is not in the set #expect(!enabledTraits.contains(EnabledTrait(name: "Pineapple", setBy: .trait("Apple")))) } + /// Tests inserting a trait that already exists (unifying setters), removing it, and verifying + /// the removed trait has the merged setters. Also tests inserting a new trait via string literal. @Test func enabledTraits_insertAndRemoveExistingTrait() throws { var enabledTraits: EnabledTraits = ["Apple", "Banana", "Orange"] @@ -191,6 +231,8 @@ struct EnabledTraitTests { #expect(enabledTraits.contains("MyStringTrait")) } + /// Verifies that removing a non-existent trait returns `nil`, and inserting a new trait + /// adds it to the set. @Test func enabledTraits_insertAndRemoveNonExistingTrait() throws { var enabledTraits: EnabledTraits = ["Banana"] @@ -208,6 +250,7 @@ struct EnabledTraitTests { #expect(enabledTraits.contains("Apple")) } + /// Tests the `map` method to transform each trait in the set by adding a new setter to each. @Test func enabledTraits_flatMapAgainstSetOfTraits() { let enabledTraits: EnabledTraits = ["Apple", "Coffee", "Cookie"] @@ -226,6 +269,8 @@ struct EnabledTraitTests { ) } + /// Verifies that unioning two sets with no overlapping traits combines them into a single larger set + /// containing the traits of both sets. @Test func enabledTraits_unionWithNewTraits() { let enabledTraits: EnabledTraits = ["Banana"] @@ -237,6 +282,8 @@ struct EnabledTraitTests { #expect(unifiedSetOfTraits == ["Banana", "Cookie", "Pancakes", "Milkshake"]) } + /// Tests unioning sets with overlapping traits, verifying that duplicate traits have their + /// setters merged correctly. @Test func enabledTraits_unionWithExistingTraits() throws { let enabledTraits: EnabledTraits = [ @@ -281,6 +328,8 @@ struct EnabledTraitTests { #expect(milkshakeTrait.setters.isEmpty) } + /// Verifies that initializing `EnabledTraits` with duplicate trait names in the array + /// results in a single trait (set behavior). @Test func enabledTraits_testInitWithArrayOfSameString() throws { var traits: EnabledTraits = [ @@ -299,13 +348,284 @@ struct EnabledTraitTests { #expect(!traits.contains(bananaTrait)) } + /// Tests that intersecting with an empty set returns an empty set. + @Test + func enabledTraits_testIntersectionWithEmptySet() { + let enabledTraits: EnabledTraits = ["Apple", "Banana", "Cheese"] + let emptyTraits = EnabledTraits() + + let intersection = enabledTraits.intersection(emptyTraits) + #expect(intersection.isEmpty) + } + + /// Verifies that intersecting with an identical set returns the same set. + @Test + func enabledTraits_testIntersectionWithIdenticalSet() { + let enabledTraits: EnabledTraits = ["Apple", "Banana", "Cheese"] + let otherEnabledTraits: EnabledTraits = ["Apple", "Banana", "Cheese"] + #expect(enabledTraits == otherEnabledTraits) + + let intersection = enabledTraits.intersection(otherEnabledTraits) + #expect(intersection == enabledTraits) + #expect(intersection == otherEnabledTraits) + } + + /// Tests intersection of two sets with partial overlap, verifying only common traits are returned. + @Test + func enabledTraits_testIntersectionWithDifferentSets() throws { + let enabledTraits: EnabledTraits = ["Apple", "Banana", "Orange"] + var otherEnabledTraits: EnabledTraits = ["Banana", "Chocolate"] + #expect(enabledTraits != otherEnabledTraits) + + let intersection = enabledTraits.intersection(otherEnabledTraits) + #expect(intersection.count == 1) + #expect(intersection.contains("Banana")) + + let bananaTrait = try otherEnabledTraits.unwrapRemove("Banana") + #expect(!otherEnabledTraits.contains(bananaTrait)) + + let newIntersection = enabledTraits.intersection(otherEnabledTraits) + #expect(newIntersection.isEmpty) + } + + /// Verifies intersection behavior with single-element sets containing the same trait. + @Test + func enabledTraits_testIntersectionWithOneElementSets() throws { + let enabledTraits: EnabledTraits = ["Apple"] + let otherEnabledTraits: EnabledTraits = [EnabledTrait(name: "Apple", setBy: .package(.init(identity: "MyFruits")))] + #expect(enabledTraits == otherEnabledTraits) + + let intersection = enabledTraits.intersection(otherEnabledTraits) + #expect(intersection.count == 1) + #expect(intersection == enabledTraits) + #expect(intersection == otherEnabledTraits) + } + // MARK: - EnabledTraitsMap Tests + + /// Tests basic initialization of an empty `EnabledTraitsMap` and verifies default trait behavior. + @Test + func enabledTraitsMap_initEmpty() { + let map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + // Accessing a non-existent package should return ["default"] + #expect(map[packageId] == ["default"]) + } + + /// Verifies that `EnabledTraitsMap` can be initialized using dictionary literal syntax. + @Test + func enabledTraitsMap_initWithDictionaryLiteral() { + let packageA = PackageIdentity(stringLiteral: "PackageA") + let packageB = PackageIdentity(stringLiteral: "PackageB") + + let map: EnabledTraitsMap = [ + packageA: ["Apple", "Banana"], + packageB: ["Coffee"] + ] + + #expect(map[packageA] == ["Apple", "Banana"]) + #expect(map[packageB] == ["Coffee"]) + } + + /// Tests that `EnabledTraitsMap` can be initialized from a dictionary. + @Test + func enabledTraitsMap_initWithDictionary() { + let packageA = PackageIdentity(stringLiteral: "PackageA") + let packageB = PackageIdentity(stringLiteral: "PackageB") + + let dictionary: [PackageIdentity: EnabledTraits] = [ + packageA: ["Apple", "Banana"], + packageB: ["Coffee"] + ] + + let map = EnabledTraitsMap(dictionary) + + #expect(map[packageA] == ["Apple", "Banana"]) + #expect(map[packageB] == ["Coffee"]) + } + + /// Verifies that setting traits via subscript adds them to the map. + @Test + func enabledTraitsMap_setTraitsViaSubscript() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + map[packageId] = ["Apple", "Banana"] + + #expect(map[packageId] == ["Apple", "Banana"]) + } + + /// Tests that setting "default" traits explicitly does not store them in the map, + /// since the map returns "default" by default for packages without explicitly + /// set traits. + @Test + func enabledTraitsMap_setDefaultTraitsDoesNotStore() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + // Setting ["default"] should be omitted from storage + map[packageId] = ["default"] + + // The package should still return ["default"] when accessed + #expect(map[packageId] == ["default"]) + + // But there should be no explicit entry in storage + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + } + + /// Verifies that setting traits multiple times on the same package unifies them + /// (forms union) rather than replacing them. + @Test + func enabledTraitsMap_multipleSetsCombineTraits() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + map[packageId] = ["Apple", "Banana"] + map[packageId] = ["Coffee", "Chocolate"] + + // Should contain all four traits + #expect(map[packageId].contains("Apple")) + #expect(map[packageId].contains("Banana")) + #expect(map[packageId].contains("Coffee")) + #expect(map[packageId].contains("Chocolate")) + #expect(map[packageId].count == 4) + } + + /// Tests that setting overlapping traits unifies the setters correctly. + @Test + func enabledTraitsMap_overlappingTraitsUnifySetters() throws { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + let parentPackage1 = PackageIdentity(stringLiteral: "Parent1") + let parentPackage2 = PackageIdentity(stringLiteral: "Parent2") + + map[packageId] = EnabledTraits([ + EnabledTrait(name: "Apple", setBy: .package(.init(identity: parentPackage1))) + ]) + + map[packageId] = EnabledTraits([ + EnabledTrait(name: "Apple", setBy: .package(.init(identity: parentPackage2))) + ]) + + var traits = map[packageId] + let appleTrait = try traits.unwrapRemove("Apple") + + // The Apple trait should have both setters + #expect(appleTrait.setters.count == 2) + #expect(appleTrait.setters.contains(.package(.init(identity: parentPackage1)))) + #expect(appleTrait.setters.contains(.package(.init(identity: parentPackage2)))) + } + + /// Verifies the `explicitlyEnabledTraitsFor` subscript returns `nil` for packages + /// without explicitly set traits. + @Test + func enabledTraitsMap_explicitlyEnabledTraitsReturnsNilForDefault() { + let map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + // No traits have been set, so explicit traits should be nil + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + + // But regular subscript should return ["default"] + #expect(map[packageId] == ["default"]) + } + + /// Tests that `explicitlyEnabledTraitsFor` returns the actual traits when they are set. + @Test + func enabledTraitsMap_explicitlyEnabledTraitsReturnsSetTraits() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + map[packageId] = ["Apple", "Banana"] + + let explicitTraits = map[explicitlyEnabledTraitsFor: packageId] + + #expect(explicitTraits != nil) + #expect(explicitTraits == ["Apple", "Banana"]) + } + + /// Verifies that `dictionaryLiteral` property returns the underlying storage as a dictionary. + @Test + func enabledTraitsMap_dictionaryLiteralReturnsStorage() { + var map = EnabledTraitsMap() + let packageA = PackageIdentity(stringLiteral: "PackageA") + let packageB = PackageIdentity(stringLiteral: "PackageB") + + map[packageA] = ["Apple", "Banana"] + map[packageB] = ["Coffee"] + + let dictionary = map.dictionaryLiteral + + #expect(dictionary.count == 2) + #expect(dictionary[packageA] == ["Apple", "Banana"]) + #expect(dictionary[packageB] == ["Coffee"]) + } + + /// Tests that after setting default traits explicitly, they are omitted from `dictionaryLiteral`. + @Test + func enabledTraitsMap_dictionaryLiteralOmitsDefaultTraits() { + var map = EnabledTraitsMap() + let packageA = PackageIdentity(stringLiteral: "PackageA") + let packageB = PackageIdentity(stringLiteral: "PackageB") + + map[packageA] = ["Apple", "Banana"] + map[packageB] = ["default"] // Should not be stored + + let dictionary = map.dictionaryLiteral + + // Only PackageA should be in the dictionary + #expect(dictionary.count == 1) + #expect(dictionary[packageA] == ["Apple", "Banana"]) + #expect(dictionary[packageB] == nil) + } + + /// Verifies behavior when mixing default and non-default traits in a single set operation. + @Test + func enabledTraitsMap_setMixedDefaultAndNonDefaultTraits() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + // Set traits including "default" + map[packageId] = ["Apple", "default", "Banana"] + + // The traits should be stored since there are non-default traits + #expect(map[packageId].contains("Apple")) + #expect(map[packageId].contains("Banana")) + #expect(map[packageId].contains("default")) + + // Should have explicit entry + #expect(map[explicitlyEnabledTraitsFor: packageId] != nil) + } + + /// Tests that multiple packages can be stored independently in the map. + @Test + func enabledTraitsMap_multiplePackagesIndependent() { + var map = EnabledTraitsMap() + let packageA = PackageIdentity(stringLiteral: "PackageA") + let packageB = PackageIdentity(stringLiteral: "PackageB") + let packageC = PackageIdentity(stringLiteral: "PackageC") + + map[packageA] = ["Apple"] + map[packageB] = ["Banana"] + // PackageC not set, should default + + #expect(map[packageA] == ["Apple"]) + #expect(map[packageB] == ["Banana"]) + #expect(map[packageC] == ["default"]) + + #expect(map[explicitlyEnabledTraitsFor: packageA] != nil) + #expect(map[explicitlyEnabledTraitsFor: packageB] != nil) + #expect(map[explicitlyEnabledTraitsFor: packageC] == nil) + } } // MARK: - Test Helpers extension EnabledTraits { - // Helper method to unwrap elements that are removed from the set. + /// Helper method that removes a trait from the set and unwraps the returned optional. + /// This method asserts that the trait exists in the set before removal, making tests + /// more concise by combining removal and nil-checking in a single operation. package mutating func unwrapRemove(_ trait: Element) throws -> Element { let optionalTrait = self.remove(trait) let trait = try #require(optionalTrait) diff --git a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift index 0c430305d45..6626e7c5ad9 100644 --- a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift +++ b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift @@ -1,9 +1,14 @@ +//===----------------------------------------------------------------------===// // -// WorkspaceTests+Traits.swift -// SwiftPM +// This source file is part of the Swift open source project // -// Created by Bri Peticca on 2025-10-22. +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception // +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// import _InternalTestSupport import Basics @@ -782,8 +787,6 @@ extension WorkspaceTests { return } - print("enabled traits: \(enabledTraits)") - let deps = package.dependencies XCTAssertEqual(deps, [PackageIdentity(urlString: "./GuardedDependency")]) XCTAssertEqual(enabledTraits, ["Enabled1", "Enabled2"]) From c8ffb4691ec0b3e96bbb1c1248a75aa9c8dc4f1a Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Fri, 31 Oct 2025 17:54:54 -0400 Subject: [PATCH 10/14] Cleanup remaining TODOs --- Sources/PackageGraph/PackageGraphRoot.swift | 6 +-- .../PackageModel+Extensions.swift | 3 +- .../Manifest/Manifest+Traits.swift | 7 +-- .../Workspace/Workspace+Dependencies.swift | 5 -- Sources/Workspace/Workspace+Manifests.swift | 24 --------- Sources/Workspace/Workspace+Traits.swift | 53 ------------------- Tests/PackageModelTests/ManifestTests.swift | 1 - 7 files changed, 4 insertions(+), 95 deletions(-) diff --git a/Sources/PackageGraph/PackageGraphRoot.swift b/Sources/PackageGraph/PackageGraphRoot.swift index 742d032515a..06038152ff8 100644 --- a/Sources/PackageGraph/PackageGraphRoot.swift +++ b/Sources/PackageGraph/PackageGraphRoot.swift @@ -162,12 +162,10 @@ public struct PackageGraphRoot { let enabledTraits = dep.traits?.filter { guard let condition = $0.condition else { return true } return condition.isSatisfied(by: rootEnabledTraits.names) - // TODO bp modify this. - }.map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: "root", name: "root"))) }) + }.map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: "root"))) }) - // TODO bp enabled traits map must flatten default traits before this var enabledTraitsSet = enabledTraitsMap[dep.identity] - enabledTraitsSet.formUnion(EnabledTraits(enabledTraits ?? [])) // TODO bp modify this. + enabledTraitsSet.formUnion(EnabledTraits(enabledTraits ?? [])) return PackageContainerConstraint( package: dep.packageRef, diff --git a/Sources/PackageGraph/PackageModel+Extensions.swift b/Sources/PackageGraph/PackageModel+Extensions.swift index e75daa9df9e..83d01874467 100644 --- a/Sources/PackageGraph/PackageModel+Extensions.swift +++ b/Sources/PackageGraph/PackageModel+Extensions.swift @@ -42,8 +42,7 @@ extension Manifest { return condition.isSatisfied(by: enabledTraits.names) }.map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: self.packageIdentity, name: self.displayName))) }) - // TODO bp enabledTraitsMap must be propagated here? - let enabledTraitsSet = EnabledTraits(explicitlyEnabledTraits ?? []) //explicitlyEnabledTraits.flatMap({ Set($0) }) ?? ["default"] + let enabledTraitsSet = EnabledTraits(explicitlyEnabledTraits ?? []) return PackageContainerConstraint( package: $0.packageRef, diff --git a/Sources/PackageModel/Manifest/Manifest+Traits.swift b/Sources/PackageModel/Manifest/Manifest+Traits.swift index 6d5fb017502..bc306e387a8 100644 --- a/Sources/PackageModel/Manifest/Manifest+Traits.swift +++ b/Sources/PackageModel/Manifest/Manifest+Traits.swift @@ -94,14 +94,10 @@ extension Manifest { /// Validates a set of traits that is intended to be enabled for the manifest; if there are any discrepencies in the /// set of enabled traits and whether the manifest defines these traits (or if it defines any traits at all), then an /// error indicating the issue will be thrown. - private func validateEnabledTraits( - _ explicitlyEnabledTraits: EnabledTraits, -// _ parentPackage: PackageIdentifier? = nil - ) throws { + private func validateEnabledTraits(_ explicitlyEnabledTraits: EnabledTraits) throws { guard supportsTraits else { if explicitlyEnabledTraits != ["default"] { throw TraitError.traitsNotSupported( -// parent: parentPackage, package: .init(self), explicitlyEnabledTraits: explicitlyEnabledTraits.map({ $0 }) ) @@ -124,7 +120,6 @@ extension Manifest { // We throw an error when default traits are disabled for a package without any traits // This allows packages to initially move new API behind traits once. throw TraitError.traitsNotSupported( -// parent: parentPackage, package: .init(self), explicitlyEnabledTraits: enabledTraits.map({ $0 }) ) diff --git a/Sources/Workspace/Workspace+Dependencies.swift b/Sources/Workspace/Workspace+Dependencies.swift index 99e6d15c289..23f327133d6 100644 --- a/Sources/Workspace/Workspace+Dependencies.swift +++ b/Sources/Workspace/Workspace+Dependencies.swift @@ -539,11 +539,6 @@ extension Workspace { observabilityScope: observabilityScope ) - // Update the traits map if we've fetched new manifests -// currentManifests.allDependencyManifests.forEach({ manifest in -// let enabledTraits = -// }) - guard !observabilityScope.errorsReported else { return currentManifests } diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 46e11e4bac0..135a0d06cd5 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -553,20 +553,6 @@ extension Workspace { let rootManifests = try root.manifests.mapValues { manifest in let parentEnabledTraits = self.enabledTraitsMap[manifest.packageIdentity] let deps = try manifest.dependencies.filter { dep in -// let explicitlyEnabledTraitsSet = dep.traits?.filter({ $0.isEnabled(by: parentEnabledTraits) }).map(\.name) -// if let explicitlyEnabledTraitsSet { -// let explicitlyEnabledTraits = EnabledTraits( -// explicitlyEnabledTraitsSet, -// setBy: .package(.init(manifest)) -// ) -// self.enabledTraitsMap[dep.identity] = explicitlyEnabledTraits -// } -// .map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: manifest.packageIdentity, name: manifest.displayName))) }) - -// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { -// self.enabledTraitsMap[dep.identity] = enabledTraitsSet -// } - let isDepUsed = try manifest.isPackageDependencyUsed(dep, enabledTraits: parentEnabledTraits) return isDepUsed } @@ -603,16 +589,6 @@ extension Workspace { let firstLevelDependencies = try topLevelManifests.values.map { manifest in let parentEnabledTraits = self.enabledTraitsMap[manifest.packageIdentity] return try manifest.dependencies.filter { dep in - let explicitlyEnabledTraitsSet = dep.traits?.filter({ $0.isEnabled(by: parentEnabledTraits)}).map(\.name) - - if let explicitlyEnabledTraitsSet { - let explicitlyEnabledTraits = EnabledTraits( - explicitlyEnabledTraitsSet, - setBy: .package(.init(manifest)) - ) - self.enabledTraitsMap[dep.identity] = explicitlyEnabledTraits - } - let isDepUsed = try manifest.isPackageDependencyUsed(dep, enabledTraits: parentEnabledTraits) return isDepUsed diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift index 27f07039a83..44615d3b5e6 100644 --- a/Sources/Workspace/Workspace+Traits.swift +++ b/Sources/Workspace/Workspace+Traits.swift @@ -58,57 +58,4 @@ extension Workspace { self.enabledTraitsMap[dependency.identity] = explicitlyEnabledTraits } } - -// public func precomputeTraits( -// _ topLevelManifests: [Manifest], -// _ manifestMap: [PackageIdentity: Manifest] -// ) throws -> [PackageIdentity: EnabledTraits] { -// var visited: Set = [] -// -// func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { -// let parentTraits = self.enabledTraitsMap[parent.packageIdentity] -// let requiredDependencies = try parent.dependenciesRequired(for: productFilter, parentTraits) -// let guardedDependencies = parent.dependenciesTraitGuarded(withEnabledTraits: parentTraits) -// -// _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in -// return try manifestMap[dependency.identity].flatMap({ manifest in -// -// let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: parentTraits) }.map(\.name) -//// .map({ EnabledTrait(name: $0.name, setBy: .package(.init(parent))) }) -// if let explicitlyEnabledTraits { -// let explicitlyEnabledTraits = EnabledTraits( -// explicitlyEnabledTraits, -// setBy: .package(.init(parent)) -// ) -// let calculatedTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) -// self.enabledTraitsMap[dependency.identity] = calculatedTraits -// } -//// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { -//// let calculatedTraits = try manifest.enabledTraits( -//// using: enabledTraitsSet -////// .init(parent) -//// ) -//// self.enabledTraitsMap[dependency.identity] = calculatedTraits -//// } -// -// let result = visited.insert(dependency.identity) -// if result.inserted { -// try dependencies(of: manifest, dependency.productFilter) -// } -// -// return manifest -// }) -// }) -// } -// -// for manifest in topLevelManifests { -// // Track already-visited manifests to avoid cycles -// let result = visited.insert(manifest.packageIdentity) -// if result.inserted { -// try dependencies(of: manifest) -// } -// } -// -// return self.enabledTraitsMap.dictionaryLiteral -// } } diff --git a/Tests/PackageModelTests/ManifestTests.swift b/Tests/PackageModelTests/ManifestTests.swift index 33926c5780d..8d8f7b00ce6 100644 --- a/Tests/PackageModelTests/ManifestTests.swift +++ b/Tests/PackageModelTests/ManifestTests.swift @@ -345,7 +345,6 @@ class ManifestTests: XCTestCase { } // If given a parent package, and the default traits are disabled: - // TODO bp need to uncover a method to deal with parent pacakge disabling dependencies' traits for this error XCTAssertThrowsError(try manifest.enabledTraits(using: [])) { error in XCTAssertEqual("\(error)", """ Disabled default traits by package 'qux' on package 'foo' (Foo) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. From 2a2d079ad490ef7f4e81e6547ce112ed36a79423 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Mon, 3 Nov 2025 20:10:37 -0500 Subject: [PATCH 11/14] Consider disbale default traits metadata; update tests * Augment EnabledTraits and EnabledTraitsMap to consider the case where a package's default traits are disabled through some parent configuration. * Update methods in Manifest+Traits.swift * Add tests for disable default traits behaviours in EnabledTraitTests.swift and WorkspaceTests+Traits.swift. --- Sources/PackageGraph/PackageGraphRoot.swift | 2 +- .../PackageModel+Extensions.swift | 4 +- Sources/PackageModel/EnabledTrait.swift | 164 +++++++++++-- .../Manifest/Manifest+Traits.swift | 106 +++++---- Sources/Workspace/Workspace+Manifests.swift | 2 +- .../_InternalTestSupport/MockWorkspace.swift | 2 +- .../PackageModelTests/EnabledTraitTests.swift | 225 ++++++++++++++++++ Tests/PackageModelTests/ManifestTests.swift | 5 +- .../WorkspaceTests+Traits.swift | 153 +++++++++++- 9 files changed, 589 insertions(+), 74 deletions(-) diff --git a/Sources/PackageGraph/PackageGraphRoot.swift b/Sources/PackageGraph/PackageGraphRoot.swift index 06038152ff8..e99ccd9d332 100644 --- a/Sources/PackageGraph/PackageGraphRoot.swift +++ b/Sources/PackageGraph/PackageGraphRoot.swift @@ -165,7 +165,7 @@ public struct PackageGraphRoot { }.map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: "root"))) }) var enabledTraitsSet = enabledTraitsMap[dep.identity] - enabledTraitsSet.formUnion(EnabledTraits(enabledTraits ?? [])) + enabledTraitsSet.formUnion(enabledTraits ?? []) return PackageContainerConstraint( package: dep.packageRef, diff --git a/Sources/PackageGraph/PackageModel+Extensions.swift b/Sources/PackageGraph/PackageModel+Extensions.swift index 83d01874467..6c4084e49e0 100644 --- a/Sources/PackageGraph/PackageModel+Extensions.swift +++ b/Sources/PackageGraph/PackageModel+Extensions.swift @@ -40,9 +40,9 @@ extension Manifest { let explicitlyEnabledTraits = $0.traits?.filter { guard let condition = $0.condition else { return true } return condition.isSatisfied(by: enabledTraits.names) - }.map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: self.packageIdentity, name: self.displayName))) }) + }.map(\.name) - let enabledTraitsSet = EnabledTraits(explicitlyEnabledTraits ?? []) + let enabledTraitsSet = EnabledTraits(explicitlyEnabledTraits ?? [], setBy: .package(.init(identity: self.packageIdentity, name: self.displayName))) return PackageContainerConstraint( package: $0.packageRef, diff --git a/Sources/PackageModel/EnabledTrait.swift b/Sources/PackageModel/EnabledTrait.swift index d411654eb6f..9e2bf6a04ab 100644 --- a/Sources/PackageModel/EnabledTrait.swift +++ b/Sources/PackageModel/EnabledTrait.swift @@ -15,33 +15,109 @@ import Basics // MARK: - EnabledTraitsMap /// A wrapper struct for a dictionary that stores the transitively enabled traits for each package. -/// This struct implicitly omits adding `default` traits to its storage, and returns `nil` if it there is no existing entry for -/// a given package, since if there are no explicitly enabled traits set by anything else a package will then default to its `default` traits, -/// if they exist. +/// This struct implicitly omits adding `default` traits to its storage, and returns `nil` if it +/// there is no existing entry for a given package, since if there are no explicitly enabled traits +/// set by anything else a package will then default to its `default` traits, if they exist. +/// +/// ## Union Behavior +/// When setting traits via the subscript setter (e.g., `map[packageId] = ["trait1", "trait2"]`), +/// the new traits are **unified** with any existing traits for that package, rather than +/// replacing them. This means multiple assignments to the same package will accumulate +/// all traits into a union. If the same trait name is set multiple times with different setters, the +/// setters are merged together. +/// +/// Example: +/// ```swift +/// var traits = EnabledTraitsMap() +/// traits[packageId] = ["Apple", "Banana"] +/// traits[packageId] = ["Coffee", "Chocolate"] +/// +/// // traits[packageId] now contains all four traits: +/// print(traits[packageId]) +/// // Output: ["Apple", "Banana", "Coffee", "Chocolate"] +/// ``` +/// +/// ## Disablers +/// When a package or trait configuration explicitly sets an empty trait set (`[]`) for another package, +/// this is tracked as a "disabler" to record the intent to disable default traits. Disablers coexist +/// with the unified trait system—a package can have both recorded disablers AND explicitly enabled +/// traits. This allows the system to distinguish between "no traits specified" versus "default traits +/// explicitly disabled but other traits may be enabled by different parents." +/// +/// Only packages (via `Setter.package`) and trait configurations (via `Setter.traitConfiguration`) +/// can disable default traits. Traits themselves cannot disable other packages' default traits. +/// +/// Example: +/// ```swift +/// var traits = EnabledTraitsMap() +/// let dependencyId = PackageIdentity(stringLiteral: "MyDependency") +/// let parent1 = PackageIdentity(stringLiteral: "Parent1") +/// let parent2 = PackageIdentity(stringLiteral: "Parent2") +/// +/// // Parent1 explicitly disables default traits +/// traits[dependencyId] = EnabledTraits([], setBy: .package(.init(identity: parent1))) +/// +/// // Parent2 enables specific traits for the same dependency +/// traits[dependencyId] = EnabledTraits(["MyTrait"], setBy: .package(.init(identity: parent2))) +/// +/// // Query disablers to see who disabled defaults +/// print(traits[disablersFor: dependencyId]) // Contains .package(Parent1) +/// +/// // The dependency has "MyTrait" trait enabled (unified from Parent2) +/// print(traits[dependencyId]) // Output: ["MyTrait"] +/// ``` public struct EnabledTraitsMap { public typealias Key = PackageIdentity public typealias Value = EnabledTraits + /// Storage for explicitly enabled traits per package. Omits packages with only the "default" trait. private var storage: ThreadSafeKeyValueStore = .init() + /// Tracks setters that explicitly disabled default traits (via []) for each package. + private var _disablers: ThreadSafeKeyValueStore> = .init() + public init() { } public subscript(key: PackageIdentity) -> EnabledTraits { get { storage[key] ?? ["default"] } set { // Omit adding "default" explicitly, since the map returns "default" - // if there is no explicit traits declared. This will allow us to check + // if there are no explicit traits enabled. This will allow us to check // for nil entries in the stored dictionary, which tells us whether - // traits have been explicitly declared. - guard newValue != ["default"] else { return } + // traits have been explicitly enabled or not. + guard newValue != .defaults else { + // If explicitly disabled default traits prior, then + // reset this in storage and assure we can still fetch defaults. + // We will still track whenever default traits were disabled by + // keeping the _disablers map as it was. + if self.storage[key] == [] { + self.storage[key] = nil + } + return + } + + // Set disablers; continue to union existing enabled traits. + if newValue.isEmpty, let disabler = newValue.disabledBy { + if !self._disablers.contains(key) { + _disablers[key] = [] + } + _disablers[key]?.insert(disabler) + } + + // If there are no explictly enabled traits added yet, then create entry. if storage[key] == nil { storage[key] = newValue } else { + // Combine the existing set of enabled traits with the newValue. storage[key]?.formUnion(newValue) } } } + public subscript(disablersFor key: PackageIdentity) -> Set? { + get { self._disablers[key] } + } + /// Returns a list of traits that were explicitly enabled for a given package. public subscript(explicitlyEnabledTraitsFor key: PackageIdentity) -> EnabledTraits? { get { storage[key] } @@ -105,10 +181,17 @@ public struct EnabledTrait: Identifiable { setters.compactMap(\.parentPackage) } + /// Returns true if this trait is the "default" trait. public var isDefault: Bool { name == "default" } + /// Returns true if this trait was enabled by the "default" trait (via `Setter.trait("default")`). + /// This is distinct from `isDefault`, which checks if this trait's name is "default". + public var isSetByDefault: Bool { + self.setters.contains(where: { $0 == .default }) + } + /// Returns a new `EnabledTrait` that contains a merged list of `Setters` from /// `self` and the `otherTrait`, only if the traits are equal. Otherwise, returns nil. /// - Parameter otherTrait: The trait to merge in. @@ -137,9 +220,9 @@ extension EnabledTrait { case .traitConfiguration: "command-line trait configuration" case .package(let parent): - "parent package: \(parent.description)" + "package \(parent.description)" case .trait(let trait): - "trait: \(trait)" + "trait \(trait)" } } @@ -153,6 +236,15 @@ extension EnabledTrait { } } + public var parentTrait: String? { + switch self { + case .trait(let trait): + return trait + case .traitConfiguration, .package: + return nil + } + } + public static var `default`: Self { .trait("default") } @@ -209,22 +301,56 @@ extension EnabledTrait: ExpressibleByStringLiteral { /// by merging their setters when inserted, maintaining a single entry per unique trait name. It provides /// convenient set operations like union and intersection, along with collection protocol conformance for /// easy iteration and manipulation of enabled traits. +/// +/// ## Disabling All Traits +/// An `EnabledTraits` instance can represent a "disabled" state when created with an empty collection +/// and a `Setter`. In this case, the `disabledBy` property returns the setter that disabled default traits, +/// allowing callers to track which parent package or configuration explicitly disabled default traits for a package. public struct EnabledTraits: Hashable { public typealias Element = EnabledTrait public typealias Index = IdentifiableSet.Index + /// Storage of enabled traits. private var _traits: IdentifiableSet = [] + /// This should only ever be set in the case where a parent + /// disables all traits, and an empty set of traits is passed. + private var _disableAllTraitsSetter: EnabledTrait.Setter? = nil + + /// Returns the setter that disabled all traits for a package, if any. + /// This value is set when `EnabledTraits` is initialized with an empty collection, + /// indicating that a parent explicitly disabled all traits rather than leaving them + /// unset. + public var disabledBy: EnabledTrait.Setter? { + _disableAllTraitsSetter + } + + public var areDefaultsEnabled: Bool { + return !_traits.filter(\.isDefault).isEmpty || !_traits.filter(\.isSetByDefault).isEmpty + } + public static var defaults: EnabledTraits { ["default"] } - public init(_ traits: C, setBy origin: EnabledTrait.Setter) where C.Element == String { - let enabledTraits = traits.map({ EnabledTrait(name: $0, setBy: origin) }) + private init(_ disabler: EnabledTrait.Setter) { + self._disableAllTraitsSetter = disabler + } + + public init(_ traits: C, setBy setter: EnabledTrait.Setter) where C.Element == String { + guard !traits.isEmpty else { + self.init(setter) + return + } + let enabledTraits = traits.map({ EnabledTrait(name: $0, setBy: setter) }) self.init(enabledTraits) } - public init(_ traits: C) where C.Element == EnabledTrait { + public init(_ enabledTraits: EnabledTraits) { + self._traits = enabledTraits._traits + } + + private init(_ traits: C) where C.Element == EnabledTrait { self._traits = IdentifiableSet(traits) } @@ -284,7 +410,7 @@ extension EnabledTraits: Collection { return EnabledTraits(intersection) } - public func union(_ other: EnabledTraits) -> EnabledTraits { + public func union(_ other: C) -> EnabledTraits where C.Element == Self.Element { let unionedTraits = _traits.union(other) return EnabledTraits(unionedTraits) } @@ -293,6 +419,10 @@ extension EnabledTraits: Collection { self._traits = self.union(other)._traits } + public mutating func formUnion(_ other: C) where C.Element == Self.Element { + self.formUnion(.init(other)) + } + public func map(_ transform: (Self.Element) throws -> Self.Element) rethrows -> EnabledTraits { let transformedTraits = try _traits.map(transform) return EnabledTraits(transformedTraits) @@ -361,20 +491,12 @@ extension IdentifiableSet where Element == EnabledTrait { } } - public func union(_ other: IdentifiableSet) -> IdentifiableSet { + package func union(_ other: C) -> IdentifiableSet where C.Element == Element { var updatedContents = self for element in other { updatedContents.insertTrait(element) } return updatedContents } - - public func union(_ other: C) -> IdentifiableSet where C.Element == Element { - if let other = other as? IdentifiableSet { - return self.union(other) - } else { - return self.union(IdentifiableSet(other.map({ $0 }))) - } - } } diff --git a/Sources/PackageModel/Manifest/Manifest+Traits.swift b/Sources/PackageModel/Manifest/Manifest+Traits.swift index bc306e387a8..061d89a3477 100644 --- a/Sources/PackageModel/Manifest/Manifest+Traits.swift +++ b/Sources/PackageModel/Manifest/Manifest+Traits.swift @@ -18,7 +18,7 @@ import Foundation /// Validator methods that check the correctness of traits and their support as defined in the manifest. extension Manifest { /// Struct that contains information about a package's identity, as well as its name. - public struct PackageIdentifier: Hashable, CustomStringConvertible { + public struct PackageIdentifier: Hashable, CustomStringConvertible, Comparable, ExpressibleByStringLiteral { public var identity: String public var name: String? @@ -36,6 +36,10 @@ extension Manifest { self.name = parent.displayName } + public init(stringLiteral string: String) { + self.identity = string + } + public var description: String { var result = "'\(identity)'" if let name { @@ -43,6 +47,10 @@ extension Manifest { } return result } + + public static func < (lhs: Manifest.PackageIdentifier, rhs: Manifest.PackageIdentifier) -> Bool { + lhs.identity < rhs.identity + } } /// Determines whether traits are supported for this Manifest. @@ -99,7 +107,7 @@ extension Manifest { if explicitlyEnabledTraits != ["default"] { throw TraitError.traitsNotSupported( package: .init(self), - explicitlyEnabledTraits: explicitlyEnabledTraits.map({ $0 }) + explicitlyEnabledTraits: explicitlyEnabledTraits ) } @@ -121,7 +129,7 @@ extension Manifest { // This allows packages to initially move new API behind traits once. throw TraitError.traitsNotSupported( package: .init(self), - explicitlyEnabledTraits: enabledTraits.map({ $0 }) + explicitlyEnabledTraits: enabledTraits ) } } @@ -132,12 +140,12 @@ extension Manifest { case .disableAllTraits: throw TraitError.traitsNotSupported( package: .init(self), - explicitlyEnabledTraits: [] + explicitlyEnabledTraits: .init([], setBy: .traitConfiguration) ) case .enabledTraits(let traits): throw TraitError.traitsNotSupported( package: .init(self), - explicitlyEnabledTraits: traits.map({ .init(stringLiteral: $0) }) + explicitlyEnabledTraits: EnabledTraits(traits, setBy: .traitConfiguration) ) case .enableAllTraits, .default: return @@ -235,21 +243,10 @@ extension Manifest { // Special case for dealing with whether a default trait is enabled. guard !trait.isDefault else { - // Check that the manifest defines default traits. + // Check that the manifest defines default traits; if so, + // determine whether the default traits are enabled. if self.traits.contains(where: \.isDefault) { - // If the trait is a default trait, then we must do the following checks: - // - If there exists a list of enabled traits, ensure that the default trait - // is declared in the set. - // - If there is no existing list of enabled traits (nil), and we know that the - // manifest has defined default traits, then just return true. - // - If none of these conditions are met, then defaults aren't enabled and we return false. - if enabledTraits.contains(trait.name) { - return true - } else if enabledTraits.isEmpty { - return true - } else { - return false - } + return enabledTraits.areDefaultsEnabled } // If manifest does not define default traits, then throw an invalid trait error. @@ -323,19 +320,16 @@ extension Manifest { } /// Computes the dependencies that are in use per target in this manifest. - public func usedTargetDependencies(withTraits enabledTraits: EnabledTraits) throws -> [String: Set] { - try self.targets.reduce(into: [String: Set]()) { depMap, target in + private func usedTargetDependencies(withTraits enabledTraits: EnabledTraits) throws -> [String: Set] { + let enabledTraits = try calculateAllEnabledTraits(explicitlyEnabledTraits: enabledTraits) + return self.targets.reduce(into: [String: Set]()) { depMap, target in let nonTraitDeps = target.dependencies.filter { $0.condition?.traits?.isEmpty ?? true } - let traitGuardedDeps = try target.dependencies.filter { dep in + let traitGuardedDeps = target.dependencies.filter { dep in let traits = dep.condition?.traits ?? [] - // If traits is empty, then we must manually validate the explicitly enabled traits. - if traits.isEmpty { - try validateEnabledTraits(enabledTraits) - } // For each trait that is a condition on this target dependency, assure that // at least one is enabled in the manifest. return !traits.intersection(enabledTraits.names).isEmpty @@ -446,13 +440,13 @@ extension Manifest { // tentatively marking the package dependency as used. to be resolved later on. return foundKnownPackage || (!foundKnownPackage && !usedDependencies.unknownPackage.isEmpty) } else { - return try !isTraitGuarded(dependency, enabledTraits: enabledTraits) + return try !isPackageDependencyTraitGuarded(dependency, enabledTraits: enabledTraits) } } /// Given a set of enabled traits, determine whether a package dependecy of this manifest is /// guarded by traits. - private func isTraitGuarded(_ dependency: PackageDependency, enabledTraits: EnabledTraits) throws -> Bool { + private func isPackageDependencyTraitGuarded(_ dependency: PackageDependency, enabledTraits: EnabledTraits) throws -> Bool { try validateEnabledTraits(enabledTraits) let targetDependenciesForPackageDependency = self.targets.flatMap({ $0.dependencies }) @@ -460,11 +454,9 @@ extension Manifest { $0.package?.caseInsensitiveCompare(dependency.identity.description) == .orderedSame }) - let enabledTraitNames = enabledTraits.names - // Determine whether the current set of enabled traits still gate the package dependency // across targets. - let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.filter({ $0.condition?.traits != nil }).allSatisfy({ self.isTraitGuarded($0, enabledTraits: enabledTraits) + let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.filter({ $0.condition?.traits != nil }).allSatisfy({ self.isTargetDependencyTraitGuarded($0, enabledTraits: enabledTraits) }) // Since we only omit a package dependency that is only guarded by traits, determine @@ -474,11 +466,10 @@ extension Manifest { return !isUsedWithoutTraitGuarding && isTraitGuarded } - private func isTraitGuarded( + private func isTargetDependencyTraitGuarded( _ dependency: TargetDescription.Dependency, enabledTraits: EnabledTraits ) -> Bool { - // Validate enabled traits guard let condition = dependency.condition, let traits = condition.traits else { return false } return enabledTraits.intersection(traits).isEmpty @@ -499,22 +490,50 @@ public enum TraitError: Swift.Error { /// traits. case traitsNotSupported( package: Manifest.PackageIdentifier, - explicitlyEnabledTraits: [EnabledTrait] + explicitlyEnabledTraits: EnabledTraits ) } extension TraitError: CustomStringConvertible { + private func generateSetterDescription(_ setters: EnabledTrait.Setters) -> String { + guard !setters.isEmpty else { + return "" + } + + var result: String = " enabled by" + if setters.count == 1, let setter = setters.first { + result += " \(setter.description)" + } else { + let parentPackages = setters.compactMap(\.parentPackage).sorted() + let parentTraits = setters.compactMap(\.parentTrait).sorted() + let traitConfiguration = setters.filter({ $0 == .traitConfiguration }).map(\.description) + + if !parentPackages.isEmpty { + result += " parent package" + result += parentPackages.count == 1 ? " " : "s " + result += parentPackages.map(\.description).joined(separator: ", ") + result += !parentTraits.isEmpty || !traitConfiguration.isEmpty ? ";" : "" + } + if !parentTraits.isEmpty { + result += " trait" + result += parentTraits.count == 1 ? " " : "s " + result += parentTraits.map(\.description).joined(separator: ", ") + result += !traitConfiguration.isEmpty ? ";" : "" + } + if !traitConfiguration.isEmpty { + result += " a custom trait configuration declared for the root" + } + } + + return result + } + public var description: String { switch self { case .invalidTrait(let package, let trait, var availableTraits): availableTraits = availableTraits.sorted() var errorMsg = "Trait '\(trait)'" - let parentPackages = Set(trait.parentPackages) - let parent: Manifest.PackageIdentifier? = parentPackages.count == 1 ? parentPackages.first : nil - - if let parent { - errorMsg += " enabled by parent package \(parent)" - } + errorMsg += generateSetterDescription(trait.setters) errorMsg += " is not declared by package \(package)." if availableTraits.isEmpty { errorMsg += " There are no available traits declared by this package." @@ -523,14 +542,13 @@ extension TraitError: CustomStringConvertible { " The available traits declared by this package are: \(availableTraits.joined(separator: ", "))." } return errorMsg - case .traitsNotSupported(let package, var explicitlyEnabledTraits): - explicitlyEnabledTraits = explicitlyEnabledTraits.sorted() + case .traitsNotSupported(let package, let explicitlyEnabledTraits): let parentPackages = Set(explicitlyEnabledTraits.compactMap(\.parentPackages).flatMap({ $0 })) let parent: Manifest.PackageIdentifier? = parentPackages.count == 1 ? parentPackages.first : nil if explicitlyEnabledTraits.isEmpty { - if let parent { + if let parent = explicitlyEnabledTraits.disabledBy { return """ - Disabled default traits by package \(parent) on package \(package) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. + Disabled default traits by \(parent.description) on package \(package) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. """ } else { return """ diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 590bdaa76c0..9664df8395e 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -593,7 +593,7 @@ extension Workspace { // the case where a package is being loaded in a wrapper project (not package), // where there are no root packages but there are dependencies. if root.packages.isEmpty { - let topLevelManifestTraits = try manifest.enabledTraits(using: parentEnabledTraits, nil) + let topLevelManifestTraits = try manifest.enabledTraits(using: parentEnabledTraits) self.enabledTraitsMap[manifest.packageIdentity] = topLevelManifestTraits } diff --git a/Sources/_InternalTestSupport/MockWorkspace.swift b/Sources/_InternalTestSupport/MockWorkspace.swift index 4e8b202654f..355dd8845c1 100644 --- a/Sources/_InternalTestSupport/MockWorkspace.swift +++ b/Sources/_InternalTestSupport/MockWorkspace.swift @@ -596,7 +596,7 @@ public final class MockWorkspace { public func checkPackageGraph( roots: [String] = [], deps: [MockDependency], - _ result: (ModulesGraph, [Basics.Diagnostic]) -> Void + _ result: (ModulesGraph, [Basics.Diagnostic]) throws -> Void ) async throws { let dependencies = try deps.map { try $0.convert( baseURL: self.packagesDir, diff --git a/Tests/PackageModelTests/EnabledTraitTests.swift b/Tests/PackageModelTests/EnabledTraitTests.swift index 47d52ab200e..14b68e111aa 100644 --- a/Tests/PackageModelTests/EnabledTraitTests.swift +++ b/Tests/PackageModelTests/EnabledTraitTests.swift @@ -618,6 +618,231 @@ struct EnabledTraitTests { #expect(map[explicitlyEnabledTraitsFor: packageB] != nil) #expect(map[explicitlyEnabledTraitsFor: packageC] == nil) } + + // MARK: - Disablers Tests + + /// Verifies that setting an empty trait set with a setter records the disabler. + /// Disablers track explicit [] assignments, which disable default traits. + @Test + func enabledTraitsMap_emptyTraitsRecordsDisabler() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + + // Parent package explicitly sets [] to disable default traits + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + // Should record the disabler + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.count == 1) + #expect(disablers?.contains(.package(.init(identity: parentPackage))) == true) + } + + /// Tests that the `disabledBy` property correctly identifies the setter that explicitly set []. + /// This tracks who disabled default traits. + @Test + func enabledTraits_disabledByIdentifiesSetter() { + let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + let traits = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + #expect(traits.isEmpty) + #expect(traits.disabledBy == .package(.init(identity: parentPackage))) + } + + /// Verifies that a non-empty trait set has no disabler. + /// Disablers only track explicit [] assignments. + @Test + func enabledTraits_nonEmptyTraitsHaveNoDisabler() { + let traits = EnabledTraits(["Apple", "Banana"], setBy: .traitConfiguration) + + #expect(!traits.isEmpty) + #expect(traits.disabledBy == nil) + } + + /// Tests that multiple disablers can be recorded for the same package. + /// Multiple parties can each explicitly disable default traits with []. + @Test + func enabledTraitsMap_multipleDisablersRecorded() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + let parentPackage1 = PackageIdentity(stringLiteral: "Parent1") + let parentPackage2 = PackageIdentity(stringLiteral: "Parent2") + + // First parent explicitly disables defaults with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) + + // Second parent also explicitly disables defaults with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage2))) + + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.count == 2) + #expect(disablers?.contains(.package(.init(identity: parentPackage1))) == true) + #expect(disablers?.contains(.package(.init(identity: parentPackage2))) == true) + } + + /// Verifies that disablers from trait configuration are recorded. + /// User can explicitly disable default traits via command line with []. + @Test + func enabledTraitsMap_traitConfigurationDisabler() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + // User explicitly disables defaults via command line with [] + map[packageId] = EnabledTraits([], setBy: .traitConfiguration) + + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.count == 1) + #expect(disablers?.contains(.traitConfiguration) == true) + } + + /// Tests that a package with no disablers returns nil for the disablers subscript. + /// Non-empty trait sets don't create disablers. + @Test + func enabledTraitsMap_noDisablersReturnsNil() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + + // Set some traits (not empty, so no disabler) + map[packageId] = EnabledTraits(["Apple"], setBy: .traitConfiguration) + + let disablers = map[disablersFor: packageId] + #expect(disablers == nil) + } + + /// Verifies that disablers track explicit disablement while traits can still be enabled by other setters. + /// This demonstrates the unified nature: a package can have both disablers AND enabled traits. + @Test + func enabledTraitsMap_disablersCoexistWithEnabledTraits() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + + // Parent package explicitly disables default traits with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + // Then trait configuration explicitly enables some traits + map[packageId] = EnabledTraits(["Apple"], setBy: .traitConfiguration) + + // Disablers should be recorded (parent disabled defaults) + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.contains(.package(.init(identity: parentPackage))) == true) + + // And traits should be present (configuration enabled traits) + #expect(map[packageId].contains("Apple")) + #expect(map[packageId].count == 1) + #expect(!map[packageId].contains("default")) + } + + /// Tests the distinction between an unset package and a package with explicitly disabled default traits. + /// Disabling (setting []) means "don't use default traits", but the package still returns defaults + /// if no other traits are explicitly enabled. + @Test + func enabledTraitsMap_distinguishUnsetVsDisabled() { + var map = EnabledTraitsMap() + let unsetPackage = PackageIdentity(stringLiteral: "UnsetPackage") + let disabledPackage = PackageIdentity(stringLiteral: "DisabledPackage") + let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + + // Parent explicitly disables default traits with [] + map[disabledPackage] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + // Unset package: never touched, no disablers + #expect(map[unsetPackage] == ["default"]) + #expect(map[explicitlyEnabledTraitsFor: unsetPackage] == nil) + #expect(map[disablersFor: unsetPackage] == nil) + + // Disabled package: explicitly set to [], has disablers, and returns empty set + #expect(map[disabledPackage] == []) + #expect(map[explicitlyEnabledTraitsFor: disabledPackage] == []) + #expect(map[disablersFor: disabledPackage] != nil) + } + + /// Verifies that initializing EnabledTraits with an empty string collection creates a disabler. + /// Empty [] means "explicitly disable default traits". + @Test + func enabledTraits_initWithEmptyCollectionCreatesDisabler() { + let emptyTraits: [String] = [] + let traits = EnabledTraits(emptyTraits, setBy: .traitConfiguration) + + #expect(traits.isEmpty) + #expect(traits.disabledBy == .traitConfiguration) + } + + /// Verifies that the same disabler set multiple times only appears once in the set. + /// Set semantics ensure unique disablers. + @Test + func enabledTraitsMap_duplicateDisablerOnlyStoredOnce() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + + // Set the same disabler multiple times + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.count == 1) + #expect(disablers?.contains(.package(.init(identity: parentPackage))) == true) + } + + /// Tests that when one package disables defaults with [] but another package enables traits + /// (including default), the unified map contains the enabled traits plus records the disabler. + @Test + func enabledTraitsMap_disablerAndEnabledTraitsCoexist() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + let parentPackage1 = PackageIdentity(stringLiteral: "Parent1") + let parentPackage2 = PackageIdentity(stringLiteral: "Parent2") + + // Parent1 explicitly disables default traits with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) + + // Parent2 enables default trait for the same package + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parentPackage2))) + + // The disabler should be recorded + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.contains(.package(.init(identity: parentPackage1))) == true) + + // And the default trait should be present in the unified set + #expect(map[packageId].contains("default")) + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + } + + /// Tests that when one package disables defaults and another enables non-default traits, + /// both the disabler and the enabled traits are tracked. + @Test + func enabledTraitsMap_disablerWithNonDefaultTraitsEnabled() { + var map = EnabledTraitsMap() + let packageId = PackageIdentity(stringLiteral: "MyPackage") + let parentPackage1 = PackageIdentity(stringLiteral: "Parent1") + let parentPackage2 = PackageIdentity(stringLiteral: "Parent2") + + // Parent1 disables defaults with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) + + // Parent2 enables specific traits + map[packageId] = EnabledTraits(["Apple", "Banana"], setBy: .package(.init(identity: parentPackage2))) + + // Disabler should be recorded + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.contains(.package(.init(identity: parentPackage1))) == true) + + // Traits should be present + #expect(map[packageId].contains("Apple")) + #expect(map[packageId].contains("Banana")) + #expect(!map[packageId].contains("default")) + #expect(map[packageId].count == 2) + #expect(map[explicitlyEnabledTraitsFor: packageId] == ["Apple", "Banana"]) + } } diff --git a/Tests/PackageModelTests/ManifestTests.swift b/Tests/PackageModelTests/ManifestTests.swift index 8d8f7b00ce6..5f9ba02d81c 100644 --- a/Tests/PackageModelTests/ManifestTests.swift +++ b/Tests/PackageModelTests/ManifestTests.swift @@ -321,7 +321,7 @@ class ManifestTests: XCTestCase { // When passed .disableAllTraits configuration XCTAssertThrowsError(try manifest.enabledTraits(using: .disableAllTraits)) { error in XCTAssertEqual("\(error)", """ - Disabled default traits on package 'foo' (Foo) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. + Disabled default traits by command-line trait configuration on package 'foo' (Foo) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. """) } @@ -345,7 +345,7 @@ class ManifestTests: XCTestCase { } // If given a parent package, and the default traits are disabled: - XCTAssertThrowsError(try manifest.enabledTraits(using: [])) { error in + XCTAssertThrowsError(try manifest.enabledTraits(using: .init([], setBy: .package("qux")))) { error in XCTAssertEqual("\(error)", """ Disabled default traits by package 'qux' on package 'foo' (Foo) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. """) @@ -465,6 +465,7 @@ class ManifestTests: XCTestCase { let enabledTraits = EnabledTraits(["Trait1"], setBy: .trait("default")) for trait in traits.sorted(by: { $0.name < $1.name }) { + // TODO bp i think default is getting failed here XCTAssertTrue(try manifest.isTraitEnabled(trait, enabledTraits)) } } diff --git a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift index 6626e7c5ad9..14e7c58a412 100644 --- a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift +++ b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift @@ -402,7 +402,7 @@ extension WorkspaceTests { try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Trait 'TraitNotFound' enabled by parent package 'foo' (Foo) is not declared by package 'baz' (Baz). The available traits declared by this package are: TraitFound."), severity: .error) + result.check(diagnostic: .equal("Trait 'TraitNotFound' enabled by package 'foo' (Foo) is not declared by package 'baz' (Baz). The available traits declared by this package are: TraitFound."), severity: .error) } } await workspace.checkManagedDependencies { result in @@ -465,7 +465,7 @@ extension WorkspaceTests { try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Trait 'TraitNotFound' is not declared by package 'foo' (Foo). The available traits declared by this package are: Trait1, Trait2, default."), severity: .error) + result.check(diagnostic: .equal("Trait 'TraitNotFound' enabled by command-line trait configuration is not declared by package 'foo' (Foo). The available traits declared by this package are: Trait1, Trait2, default."), severity: .error) } } } @@ -577,6 +577,9 @@ extension WorkspaceTests { } } + /// Tests that different trait configurations correctly control which conditional dependencies are included. + /// Verifies that enabling different traits (BreakfastOfChampions vs Healthy) includes different + /// dependencies, and that both are included with `enableAllTraits` while neither is included with `disableAllTraits`. func testTraitsConditionalDependencies() async throws { let sandbox = AbsolutePath("/tmp/ws/") let fs = InMemoryFileSystem() @@ -700,6 +703,10 @@ extension WorkspaceTests { } } + /// Tests that default traits of a dependency package are automatically enabled when + //// the parent doesn't specify traits. + /// Verifies that the default trait enables its configured traits (Enabled1 and + /// Enabled2), which in turn enables trait-guarded dependencies in the dependency's package graph. func testDefaultTraitsEnabledInPackageDependency() async throws { let sandbox = AbsolutePath("/tmp/ws/") let fs = InMemoryFileSystem() @@ -794,4 +801,146 @@ extension WorkspaceTests { } } } + + /// Tests the unified trait system where one parent disables default traits with [] + /// while another parent doesn't specify traits (defaults to default traits). + /// The resulting EnabledTraitsMap should have both disablers AND enabled default traits. + func testDisablersCoexistWithDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "RootTarget", + dependencies: [ + .product(name: "Parent1Product", package: "Parent1"), + .product(name: "Parent2Product", package: "Parent2"), + ] + ), + ], + products: [ + MockProduct(name: "RootProduct", modules: ["RootTarget"]) + ], + dependencies: [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + ), + ], + packages: [ + MockPackage( + name: "Parent1", + targets: [ + MockTarget( + name: "Parent1Target", + dependencies: [ + .product(name: "ChildProduct", package: "ChildPackage") + ] + ), + ], + products: [ + MockProduct(name: "Parent1Product", modules: ["Parent1Target"]) + ], + dependencies: [ + // Parent1 explicitly disables ChildPackage's traits with [] + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0"), traits: []) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "Parent2", + targets: [ + MockTarget( + name: "Parent2Target", + dependencies: [ + .product(name: "ChildProduct", package: "ChildPackage") + ] + ), + ], + products: [ + MockProduct(name: "Parent2Product", modules: ["Parent2Target"]) + ], + dependencies: [ + // Parent2 doesn't specify traits, so ChildPackage defaults to default traits + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "ChildPackage", + targets: [ + MockTarget( + name: "ChildTarget", + dependencies: [ + .product( + name: "GuardedProduct", + package: "GuardedDependency", + condition: .init(traits: ["Feature1"]) + ) + ] + ), + ], + products: [ + MockProduct(name: "ChildProduct", modules: ["ChildTarget"]) + ], + dependencies: [ + .sourceControl(path: "./GuardedDependency", requirement: .upToNextMajor(from: "1.0.0")) + ], + traits: [ + "Feature1", + "Feature2", + TraitDescription(name: "default", enabledTraits: ["Feature1"]) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "GuardedDependency", + targets: [ + MockTarget(name: "GuardedTarget") + ], + products: [ + MockProduct(name: "GuardedProduct", modules: ["GuardedTarget"]) + ], + versions: ["1.0.0"] + ) + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Parent1", requirement: .exact("1.0.0")), + .sourceControl(path: "./Parent2", requirement: .exact("1.0.0")), + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "RootPackage") + result.check(packages: "RootPackage", "Parent1", "Parent2", "ChildPackage", "GuardedDependency") + + // Verify ChildPackage has default traits enabled (from Parent2) + result.checkPackage("ChildPackage") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on ChildPackage") + return + } + + // Should contain Feature1 from default trait (enabled by Parent2) + XCTAssertEqual(enabledTraits, ["Feature1"]) + + // Verify the dependency on GuardedDependency is included + let deps = package.dependencies + XCTAssertEqual(deps, [PackageIdentity(urlString: "./GuardedDependency")]) + } + + // The graph should include GuardedDependency since Feature1 is enabled + result.check(modules: "RootTarget", "Parent1Target", "Parent2Target", "ChildTarget", "GuardedTarget") + } + } + } } From 8ed9b1e1173290648ecd82f60e308b13061fe2e5 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 4 Nov 2025 15:39:13 -0500 Subject: [PATCH 12/14] Track packages that use dependencies with default traits --- Sources/PackageModel/EnabledTrait.swift | 79 +++- Sources/Workspace/Workspace+Manifests.swift | 30 +- Sources/Workspace/Workspace+Traits.swift | 44 ++- .../PackageModelTests/EnabledTraitTests.swift | 348 +++++++++++++----- .../WorkspaceTests+Traits.swift | 265 ++++++++++++- 5 files changed, 628 insertions(+), 138 deletions(-) diff --git a/Sources/PackageModel/EnabledTrait.swift b/Sources/PackageModel/EnabledTrait.swift index 9e2bf6a04ab..9186fe1ee85 100644 --- a/Sources/PackageModel/EnabledTrait.swift +++ b/Sources/PackageModel/EnabledTrait.swift @@ -76,8 +76,17 @@ public struct EnabledTraitsMap { /// Tracks setters that explicitly disabled default traits (via []) for each package. private var _disablers: ThreadSafeKeyValueStore> = .init() + /// Tracks setters that requested default traits for each package. + /// This is used when a parent doesn't specify traits, meaning it wants the dependency to use its defaults. + private var _defaultSetters: ThreadSafeKeyValueStore> = .init() + public init() { } + public subscript(key: String) -> EnabledTraits { + get { self[PackageIdentity(key)] } + set { self[PackageIdentity(key)] = newValue } + } + public subscript(key: PackageIdentity) -> EnabledTraits { get { storage[key] ?? ["default"] } set { @@ -85,18 +94,15 @@ public struct EnabledTraitsMap { // if there are no explicit traits enabled. This will allow us to check // for nil entries in the stored dictionary, which tells us whether // traits have been explicitly enabled or not. - guard newValue != .defaults else { - // If explicitly disabled default traits prior, then - // reset this in storage and assure we can still fetch defaults. - // We will still track whenever default traits were disabled by - // keeping the _disablers map as it was. - if self.storage[key] == [] { - self.storage[key] = nil - } + // + // However, if "default" is explicitly set by a parent (has setters), + // track it in the `defaultSetters` property. + guard !(newValue == .defaults && !newValue.isExplicitlySetDefault) else { return } - // Set disablers; continue to union existing enabled traits. + // Track setter that disabled all default traits; + // continue to union existing enabled traits. if newValue.isEmpty, let disabler = newValue.disabledBy { if !self._disablers.contains(key) { _disablers[key] = [] @@ -104,6 +110,27 @@ public struct EnabledTraitsMap { _disablers[key]?.insert(disabler) } + // Check if this is an explicitly-set "default" trait (parent wants defaults enabled) + if newValue.isExplicitlySetDefault { + // Track that this parent wants default traits, but don't store the sentinel "default" + // The actual default traits will be resolved when the dependency's manifest is loaded + if let defaultSetter = newValue.first?.setters.first { + if !self._defaultSetters.contains(key) { + _defaultSetters[key] = [] + } + _defaultSetters[key]?.insert(defaultSetter) + } + + // If explicitly disabled default traits prior, then + // reset this in storage and assure we can still fetch defaults. + // We will still track whenever default traits were disabled by + // keeping the _disablers map as it was. + if self.storage[key] == [] { + self.storage[key] = nil + } + return + } + // If there are no explictly enabled traits added yet, then create entry. if storage[key] == nil { storage[key] = newValue @@ -118,11 +145,29 @@ public struct EnabledTraitsMap { get { self._disablers[key] } } + public subscript(disablersFor key: String) -> Set? { + self[disablersFor: .init(key)] + } + + public subscript(defaultSettersFor key: PackageIdentity) -> Set? { + get { self._defaultSetters[key] } + } + + public subscript(defaultSettersFor key: String) -> Set? { + self[defaultSettersFor: .init(key)] + } + /// Returns a list of traits that were explicitly enabled for a given package. public subscript(explicitlyEnabledTraitsFor key: PackageIdentity) -> EnabledTraits? { get { storage[key] } } + /// Returns a list of traits that were explicitly enabled for a given package. + public subscript(explicitlyEnabledTraitsFor key: String) -> EnabledTraits? { + self[explicitlyEnabledTraitsFor: .init(key)] + } + + /// Returns a dictionary literal representation of the map. public var dictionaryLiteral: [PackageIdentity: EnabledTraits] { return storage.get() @@ -137,6 +182,13 @@ extension EnabledTraitsMap: ExpressibleByDictionaryLiteral { } } + public init(_ dictionary: [String: Value]) { + let mappedDictionary = dictionary.reduce(into: [Key: Value]()) { result, element in + result[PackageIdentity(element.key)] = element.value + } + self.storage = .init(mappedDictionary) + } + public init(_ dictionary: [Key: Value]) { self.storage = .init(dictionary) } @@ -329,6 +381,15 @@ public struct EnabledTraits: Hashable { return !_traits.filter(\.isDefault).isEmpty || !_traits.filter(\.isSetByDefault).isEmpty } + /// Returns true if this represents an explicitly-set "default" trait (with setters), + /// as opposed to the sentinel `.defaults` value (no setters). + /// This is used to distinguish when a parent explicitly wants default traits enabled + /// versus when no traits have been specified at all. + public var isExplicitlySetDefault: Bool { + // Check if this equals .defaults (only contains "default" trait) AND has explicit setters + return self == .defaults && _traits.contains(where: { !$0.setters.isEmpty }) + } + public static var defaults: EnabledTraits { ["default"] } diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 9664df8395e..5c6093268e0 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -632,28 +632,6 @@ extension Workspace { dependenciesManifests.forEach { loadedManifests[$0.key] = $0.value } return try dependenciesRequired.compactMap { dependency in return try loadedManifests[dependency.identity].flatMap { manifest in - -// let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: node.item.enabledTraits)}.map(\.name) - -// .map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: node.item.identity, name: node.item.manifest.displayName)))}) - -// if let explicitlyEnabledTraits { -// let explicitlyEnabledTraits = EnabledTraits( -// explicitlyEnabledTraits, -// setBy: .package(.init(node.item.manifest)) -// ) -// let calculatedTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) -// self.enabledTraitsMap[dependency.identity] = calculatedTraits -// } - -// if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { -// let calculatedTraits = try manifest.enabledTraits( -// using: enabledTraitsSet -//// .init(node.item.manifest) -// ) -// self.enabledTraitsMap[dependency.identity] = calculatedTraits -// } - // we also compare the location as this function may attempt to load // dependencies that have the same identity but from a different location // which is an error case we diagnose an report about in the GraphLoading part which @@ -698,6 +676,14 @@ extension Workspace { } } + // Second pass: Update enabled traits for dependencies now that we have all manifests loaded + // This resolves the race condition where parents might set traits for dependencies + // before the dependency manifest is loaded and its default traits are known. + let allManifests = allNodes.mapValues(\.manifest) + for (_, manifest) in allManifests { + try updateEnabledTraits(for: manifest) + } + let dependencyManifests = allNodes.filter { !$0.value.manifest.packageKind.isRoot } // TODO: this check should go away when introducing explicit overrides diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift index 44615d3b5e6..75fb0f4a881 100644 --- a/Sources/Workspace/Workspace+Traits.swift +++ b/Sources/Workspace/Workspace+Traits.swift @@ -36,6 +36,23 @@ extension Workspace { let enabledTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits + // Check if any parents requested default traits for this package + // If so, expand the default traits and union them with existing traits + if let defaultSetters = self.enabledTraitsMap[defaultSettersFor: manifest.packageIdentity], + !defaultSetters.isEmpty { + // Calculate what the default traits are for this manifest + let defaultTraits = try manifest.enabledTraits(using: .defaults) + + // Create enabled traits for each setter that requested defaults + for setter in defaultSetters { + let traitsFromSetter = EnabledTraits( + defaultTraits.map(\.name), + setBy: setter + ) + self.enabledTraitsMap[manifest.packageIdentity] = traitsFromSetter + } + } + // Check enabled traits for the dependencies for dep in manifest.dependencies { updateEnabledTraits(forDependency: dep, manifest) @@ -44,18 +61,33 @@ extension Workspace { /// Update the enabled traits for a `PackageDependency` of a given parent `Manifest`. /// - /// This is only called if a loaded `Manifest` has package dependencies in which it sets - /// an explicit list of enabled traits for that dependency. + /// This is called when a manifest is loaded to register the parent's trait requirements for its dependencies. + /// When a parent doesn't specify traits, this explicitly registers that the parent wants the dependency + /// to use its default traits, with the parent as the setter. private func updateEnabledTraits(forDependency dependency: PackageDependency, _ parent: Manifest) { let parentEnabledTraits = self.enabledTraitsMap[parent.packageIdentity] - let explicitlyEnabledTraits = dependency.traits?.filter { $0.isEnabled(by: parentEnabledTraits)}.map(\.name) - if let explicitlyEnabledTraits { - let explicitlyEnabledTraits = EnabledTraits( + if let dependencyTraits = dependency.traits { + // Parent explicitly specified traits (could be [] to disable, or a list of specific traits) + let explicitlyEnabledTraits = dependencyTraits + .filter { $0.isEnabled(by: parentEnabledTraits) } + .map(\.name) + + let enabledTraits = EnabledTraits( explicitlyEnabledTraits, setBy: .package(.init(parent)) ) - self.enabledTraitsMap[dependency.identity] = explicitlyEnabledTraits + self.enabledTraitsMap[dependency.identity] = enabledTraits + } else { + // Parent didn't specify traits - it wants the dependency to use its defaults. + // Explicitly register "default" with this parent as the setter. + // This ensures the union system properly tracks that this parent wants defaults enabled, + // even if other parents have disabled traits. + let defaultTraits = EnabledTraits( + ["default"], + setBy: .package(.init(parent)) + ) + self.enabledTraitsMap[dependency.identity] = defaultTraits } } } diff --git a/Tests/PackageModelTests/EnabledTraitTests.swift b/Tests/PackageModelTests/EnabledTraitTests.swift index 14b68e111aa..9c58c0c6f3e 100644 --- a/Tests/PackageModelTests/EnabledTraitTests.swift +++ b/Tests/PackageModelTests/EnabledTraitTests.swift @@ -14,10 +14,13 @@ import Testing import struct PackageModel.EnabledTrait import struct PackageModel.EnabledTraits import struct PackageModel.EnabledTraitsMap -import struct PackageModel.PackageIdentity -import class PackageModel.Manifest +//import struct PackageModel.PackageIdentity -@Suite +@Suite( + .tags( + .TestSize.small + ) +) struct EnabledTraitTests { // MARK: - EnabledTrait Tests @@ -144,13 +147,8 @@ struct EnabledTraitTests { ]) let parentPackagesFromTrait = traitSetByPackages.parentPackages - let parentPackages = Set([ - .init(identity: "Cafe"), - .init(identity: "Home"), - .init(identity: "Breakfast") - ]) - #expect(Set(parentPackagesFromTrait) == parentPackages) + #expect(Set(parentPackagesFromTrait) == ["Cafe", "Home", "Breakfast"]) } // MARK: - EnabledTraits Tests @@ -401,59 +399,92 @@ struct EnabledTraitTests { #expect(intersection == otherEnabledTraits) } + /// Verifies that isExplicitlySetDefault returns true when "default" is set with an explicit setter + @Test + func enabledTraits_isExplicitlySetDefaultWithSetter() { + let defaultWithSetter = EnabledTraits( + ["default"], + setBy: .package("Package") + ) + + #expect(defaultWithSetter.isExplicitlySetDefault == true) + } + + /// Verifies that isExplicitlySetDefault returns false for the sentinel .defaults value + @Test + func enabledTraits_isExplicitlySetDefaultForSentinel() { + let sentinelDefault = EnabledTraits.defaults + + #expect(sentinelDefault.isExplicitlySetDefault == false) + } + + /// Verifies that isExplicitlySetDefault returns false for non-default traits + @Test + func enabledTraits_isExplicitlySetDefaultForNonDefault() { + let feature = EnabledTraits( + ["Feature1"], + setBy: .package("Package") + ) + + #expect(feature.isExplicitlySetDefault == false) + } + + /// Verifies that isExplicitlySetDefault returns false for multiple traits including default + @Test + func enabledTraits_isExplicitlySetDefaultWithMultipleTraits() { + let mixed = EnabledTraits( + ["default", "Feature1"], + setBy: .package("Package") + ) + + #expect(mixed.isExplicitlySetDefault == false) + } + // MARK: - EnabledTraitsMap Tests /// Tests basic initialization of an empty `EnabledTraitsMap` and verifies default trait behavior. @Test func enabledTraitsMap_initEmpty() { let map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") // Accessing a non-existent package should return ["default"] - #expect(map[packageId] == ["default"]) + #expect(map["PackageNotFound"] == ["default"]) } /// Verifies that `EnabledTraitsMap` can be initialized using dictionary literal syntax. @Test func enabledTraitsMap_initWithDictionaryLiteral() { - let packageA = PackageIdentity(stringLiteral: "PackageA") - let packageB = PackageIdentity(stringLiteral: "PackageB") - let map: EnabledTraitsMap = [ - packageA: ["Apple", "Banana"], - packageB: ["Coffee"] + "PackageA": ["Apple", "Banana"], + "PackageB": ["Coffee"] ] - #expect(map[packageA] == ["Apple", "Banana"]) - #expect(map[packageB] == ["Coffee"]) + #expect(map["PackageA"] == ["Apple", "Banana"]) + #expect(map["PackageB"] == ["Coffee"]) } /// Tests that `EnabledTraitsMap` can be initialized from a dictionary. @Test func enabledTraitsMap_initWithDictionary() { - let packageA = PackageIdentity(stringLiteral: "PackageA") - let packageB = PackageIdentity(stringLiteral: "PackageB") - - let dictionary: [PackageIdentity: EnabledTraits] = [ - packageA: ["Apple", "Banana"], - packageB: ["Coffee"] + let dictionary: [String: EnabledTraits] = [ + "PackageA": ["Apple", "Banana"], + "PackageB": ["Coffee"] ] let map = EnabledTraitsMap(dictionary) - #expect(map[packageA] == ["Apple", "Banana"]) - #expect(map[packageB] == ["Coffee"]) + #expect(map["PackageA"] == ["Apple", "Banana"]) + #expect(map["PackageB"] == ["Coffee"]) } /// Verifies that setting traits via subscript adds them to the map. @Test func enabledTraitsMap_setTraitsViaSubscript() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - map[packageId] = ["Apple", "Banana"] + map["MyPackage"] = ["Apple", "Banana"] - #expect(map[packageId] == ["Apple", "Banana"]) + #expect(map["MyPackage"] == ["Apple", "Banana"]) } /// Tests that setting "default" traits explicitly does not store them in the map, @@ -462,16 +493,15 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_setDefaultTraitsDoesNotStore() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") // Setting ["default"] should be omitted from storage - map[packageId] = ["default"] + map["MyPackage"] = ["default"] // The package should still return ["default"] when accessed - #expect(map[packageId] == ["default"]) + #expect(map["MyPackage"] == ["default"]) // But there should be no explicit entry in storage - #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + #expect(map[explicitlyEnabledTraitsFor: "MyPackage"] == nil) } /// Verifies that setting traits multiple times on the same package unifies them @@ -479,26 +509,25 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_multipleSetsCombineTraits() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - map[packageId] = ["Apple", "Banana"] - map[packageId] = ["Coffee", "Chocolate"] + map["MyPackage"] = ["Apple", "Banana"] + map["MyPackage"] = ["Coffee", "Chocolate"] // Should contain all four traits - #expect(map[packageId].contains("Apple")) - #expect(map[packageId].contains("Banana")) - #expect(map[packageId].contains("Coffee")) - #expect(map[packageId].contains("Chocolate")) - #expect(map[packageId].count == 4) + #expect(map["MyPackage"].contains("Apple")) + #expect(map["MyPackage"].contains("Banana")) + #expect(map["MyPackage"].contains("Coffee")) + #expect(map["MyPackage"].contains("Chocolate")) + #expect(map["MyPackage"].count == 4) } /// Tests that setting overlapping traits unifies the setters correctly. @Test func enabledTraitsMap_overlappingTraitsUnifySetters() throws { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - let parentPackage1 = PackageIdentity(stringLiteral: "Parent1") - let parentPackage2 = PackageIdentity(stringLiteral: "Parent2") + let packageId = "MyPackage" + let parentPackage1 = "Parent1" + let parentPackage2 = "Parent2" map[packageId] = EnabledTraits([ EnabledTrait(name: "Apple", setBy: .package(.init(identity: parentPackage1))) @@ -522,7 +551,7 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_explicitlyEnabledTraitsReturnsNilForDefault() { let map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") + let packageId = "MyPackage" // No traits have been set, so explicit traits should be nil #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) @@ -535,7 +564,7 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_explicitlyEnabledTraitsReturnsSetTraits() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") + let packageId = "MyPackage" map[packageId] = ["Apple", "Banana"] @@ -549,42 +578,38 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_dictionaryLiteralReturnsStorage() { var map = EnabledTraitsMap() - let packageA = PackageIdentity(stringLiteral: "PackageA") - let packageB = PackageIdentity(stringLiteral: "PackageB") - map[packageA] = ["Apple", "Banana"] - map[packageB] = ["Coffee"] + map["PackageA"] = ["Apple", "Banana"] + map["PackageB"] = ["Coffee"] let dictionary = map.dictionaryLiteral #expect(dictionary.count == 2) - #expect(dictionary[packageA] == ["Apple", "Banana"]) - #expect(dictionary[packageB] == ["Coffee"]) + #expect(dictionary["PackageA"] == ["Apple", "Banana"]) + #expect(dictionary["PackageB"] == ["Coffee"]) } /// Tests that after setting default traits explicitly, they are omitted from `dictionaryLiteral`. @Test func enabledTraitsMap_dictionaryLiteralOmitsDefaultTraits() { var map = EnabledTraitsMap() - let packageA = PackageIdentity(stringLiteral: "PackageA") - let packageB = PackageIdentity(stringLiteral: "PackageB") - map[packageA] = ["Apple", "Banana"] - map[packageB] = ["default"] // Should not be stored + map["PackageA"] = ["Apple", "Banana"] + map["PackageB"] = ["default"] // Should not be stored let dictionary = map.dictionaryLiteral // Only PackageA should be in the dictionary #expect(dictionary.count == 1) - #expect(dictionary[packageA] == ["Apple", "Banana"]) - #expect(dictionary[packageB] == nil) + #expect(dictionary["PackageA"] == ["Apple", "Banana"]) + #expect(dictionary["PackageB"] == nil) } /// Verifies behavior when mixing default and non-default traits in a single set operation. @Test func enabledTraitsMap_setMixedDefaultAndNonDefaultTraits() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") + let packageId = "MyPackage" // Set traits including "default" map[packageId] = ["Apple", "default", "Banana"] @@ -602,9 +627,9 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_multiplePackagesIndependent() { var map = EnabledTraitsMap() - let packageA = PackageIdentity(stringLiteral: "PackageA") - let packageB = PackageIdentity(stringLiteral: "PackageB") - let packageC = PackageIdentity(stringLiteral: "PackageC") + let packageA = "PackageA" + let packageB = "PackageB" + let packageC = "PackageC" map[packageA] = ["Apple"] map[packageB] = ["Banana"] @@ -624,26 +649,25 @@ struct EnabledTraitTests { /// Verifies that setting an empty trait set with a setter records the disabler. /// Disablers track explicit [] assignments, which disable default traits. @Test - func enabledTraitsMap_emptyTraitsRecordsDisabler() { + func enabledTraitsMap_emptyTraitsRecordsDisabler() throws { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + let packageId = "MyPackage" + let parentPackage = "ParentPackage" // Parent package explicitly sets [] to disable default traits map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) // Should record the disabler - let disablers = map[disablersFor: packageId] - #expect(disablers != nil) - #expect(disablers?.count == 1) - #expect(disablers?.contains(.package(.init(identity: parentPackage))) == true) + let disablers = try #require(map[disablersFor: packageId]) + #expect(disablers.count == 1) + #expect(disablers.first == .package(.init(identity: parentPackage))) } /// Tests that the `disabledBy` property correctly identifies the setter that explicitly set []. /// This tracks who disabled default traits. @Test func enabledTraits_disabledByIdentifiesSetter() { - let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + let parentPackage = "ParentPackage" let traits = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) #expect(traits.isEmpty) @@ -663,11 +687,11 @@ struct EnabledTraitTests { /// Tests that multiple disablers can be recorded for the same package. /// Multiple parties can each explicitly disable default traits with []. @Test - func enabledTraitsMap_multipleDisablersRecorded() { + func enabledTraitsMap_multipleDisablersRecorded() throws { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - let parentPackage1 = PackageIdentity(stringLiteral: "Parent1") - let parentPackage2 = PackageIdentity(stringLiteral: "Parent2") + let packageId = "MyPackage" + let parentPackage1 = "Parent1" + let parentPackage2 = "Parent2" // First parent explicitly disables defaults with [] map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) @@ -675,27 +699,25 @@ struct EnabledTraitTests { // Second parent also explicitly disables defaults with [] map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage2))) - let disablers = map[disablersFor: packageId] - #expect(disablers != nil) - #expect(disablers?.count == 2) - #expect(disablers?.contains(.package(.init(identity: parentPackage1))) == true) - #expect(disablers?.contains(.package(.init(identity: parentPackage2))) == true) + let disablers = try #require(map[disablersFor: packageId]) + #expect(disablers.count == 2) + #expect(disablers.contains(.package(.init(identity: parentPackage1))) == true) + #expect(disablers.contains(.package(.init(identity: parentPackage2))) == true) } /// Verifies that disablers from trait configuration are recorded. /// User can explicitly disable default traits via command line with []. @Test - func enabledTraitsMap_traitConfigurationDisabler() { + func enabledTraitsMap_traitConfigurationDisabler() throws { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") + let packageId = "MyPackage" // User explicitly disables defaults via command line with [] map[packageId] = EnabledTraits([], setBy: .traitConfiguration) - let disablers = map[disablersFor: packageId] - #expect(disablers != nil) - #expect(disablers?.count == 1) - #expect(disablers?.contains(.traitConfiguration) == true) + let disablers = try #require(map[disablersFor: packageId]) + #expect(disablers.count == 1) + #expect(disablers.contains(.traitConfiguration) == true) } /// Tests that a package with no disablers returns nil for the disablers subscript. @@ -703,7 +725,7 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_noDisablersReturnsNil() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") + let packageId = "MyPackage" // Set some traits (not empty, so no disabler) map[packageId] = EnabledTraits(["Apple"], setBy: .traitConfiguration) @@ -717,8 +739,8 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_disablersCoexistWithEnabledTraits() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + let packageId = "MyPackage" + let parentPackage = "ParentPackage" // Parent package explicitly disables default traits with [] map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) @@ -743,9 +765,9 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_distinguishUnsetVsDisabled() { var map = EnabledTraitsMap() - let unsetPackage = PackageIdentity(stringLiteral: "UnsetPackage") - let disabledPackage = PackageIdentity(stringLiteral: "DisabledPackage") - let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + let unsetPackage = "UnsetPackage" + let disabledPackage = "DisabledPackage" + let parentPackage = "ParentPackage" // Parent explicitly disables default traits with [] map[disabledPackage] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) @@ -777,8 +799,8 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_duplicateDisablerOnlyStoredOnce() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - let parentPackage = PackageIdentity(stringLiteral: "ParentPackage") + let packageId = "MyPackage" + let parentPackage = "ParentPackage" // Set the same disabler multiple times map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) @@ -796,9 +818,9 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_disablerAndEnabledTraitsCoexist() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - let parentPackage1 = PackageIdentity(stringLiteral: "Parent1") - let parentPackage2 = PackageIdentity(stringLiteral: "Parent2") + let packageId = "MyPackage" + let parentPackage1 = "Parent1" + let parentPackage2 = "Parent2" // Parent1 explicitly disables default traits with [] map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) @@ -811,7 +833,8 @@ struct EnabledTraitTests { #expect(disablers != nil) #expect(disablers?.contains(.package(.init(identity: parentPackage1))) == true) - // And the default trait should be present in the unified set + // And the default trait should be returned from the map, + // but not included in the explicitly enabled traits set itself. #expect(map[packageId].contains("default")) #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) } @@ -821,9 +844,9 @@ struct EnabledTraitTests { @Test func enabledTraitsMap_disablerWithNonDefaultTraitsEnabled() { var map = EnabledTraitsMap() - let packageId = PackageIdentity(stringLiteral: "MyPackage") - let parentPackage1 = PackageIdentity(stringLiteral: "Parent1") - let parentPackage2 = PackageIdentity(stringLiteral: "Parent2") + let packageId = "MyPackage" + let parentPackage1 = "Parent1" + let parentPackage2 = "Parent2" // Parent1 disables defaults with [] map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) @@ -843,6 +866,131 @@ struct EnabledTraitTests { #expect(map[packageId].count == 2) #expect(map[explicitlyEnabledTraitsFor: packageId] == ["Apple", "Banana"]) } + + // MARK: - Default Setters Tests + + /// Verifies that explicitly-set defaults are tracked in _defaultSetters and not stored + @Test + func enabledTraitsMap_defaultSettersTrackedNotStored() { + var map = EnabledTraitsMap() + let packageId = "ChildPackage" + let parentId = "ParentPackage" + + // Parent explicitly sets default + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parentId))) + + // Default setter should be tracked + let defaultSetters = map[defaultSettersFor: packageId] + #expect(defaultSetters != nil) + #expect(defaultSetters?.contains(.package(.init(identity: parentId))) == true) + + // But "default" should NOT be in storage + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + + // The getter should still return ["default"] (sentinel value) + #expect(map[packageId] == ["default"]) + } + + /// Verifies that multiple parents can set defaults and all are tracked + @Test + func enabledTraitsMap_multipleDefaultSetters() { + var map = EnabledTraitsMap() + let packageId = "ChildPackage" + let parent1 = "Parent1" + let parent2 = "Parent2" + + // Both parents set defaults + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parent1))) + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parent2))) + + // Both should be tracked + let defaultSetters = map[defaultSettersFor: packageId] + #expect(defaultSetters?.count == 2) + #expect(defaultSetters?.contains(.package(.init(identity: parent1))) == true) + #expect(defaultSetters?.contains(.package(.init(identity: parent2))) == true) + } + + /// Verifies that default setters coexist with disablers + @Test + func enabledTraitsMap_defaultSettersCoexistWithDisablers() { + var map = EnabledTraitsMap() + let packageId = "ChildPackage" + let parent1 = "Parent1" + let parent2 = "Parent2" + + // Parent1 disables traits + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parent1))) + + // Parent2 wants defaults + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parent2))) + + // Both should be tracked independently + let disablers = map[disablersFor: packageId] + let defaultSetters = map[defaultSettersFor: packageId] + + #expect(disablers?.contains(.package(.init(identity: parent1))) == true) + #expect(defaultSetters?.contains(.package(.init(identity: parent2))) == true) + } + + /// Verifies that default setters coexist with explicit traits + @Test + func enabledTraitsMap_defaultSettersCoexistWithExplicitTraits() { + var map = EnabledTraitsMap() + let packageId = "ChildPackage" + let parent1 = "Parent1" + let parent2 = "Parent2" + + // Parent1 explicitly enables Feature1 + map[packageId] = EnabledTraits(["Feature1"], setBy: .package(.init(identity: parent1))) + + // Parent2 wants defaults + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parent2))) + + // Default setters should be tracked + let defaultSetters = map[defaultSettersFor: packageId] + #expect(defaultSetters?.contains(.package(.init(identity: parent2))) == true) + + // And explicit traits should be stored + #expect(map[packageId].contains("Feature1")) + } + + /// Verifies that setting sentinel .defaults doesn't create a default setter + @Test + func enabledTraitsMap_sentinelDefaultsDoesNotCreateSetter() { + var map = EnabledTraitsMap() + let packageId = "Package" + + // Set sentinel .defaults + map[packageId] = .defaults + + // No default setters should be recorded + #expect(map[defaultSettersFor: packageId] == nil) + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + } + + /// Verifies that traitConfiguration can also set defaults and be tracked + @Test + func enabledTraitsMap_traitConfigurationAsDefaultSetter() throws { + var map = EnabledTraitsMap() + let packageId = "Package" + + // Trait configuration sets default + map[packageId] = EnabledTraits(["default"], setBy: .traitConfiguration) + + // Should be tracked + let defaultSetters = try #require(map[defaultSettersFor: packageId]) + #expect(defaultSetters.contains(.traitConfiguration) == true) + } + + /// Verifies that no default setters exist for unset packages + @Test + func enabledTraitsMap_noDefaultSettersForUnsetPackage() { + let map = EnabledTraitsMap() + let packageId = "UnsetPackage" + + // Never touched + #expect(map[defaultSettersFor: packageId] == nil) + } } diff --git a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift index 14e7c58a412..4cad90c1de3 100644 --- a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift +++ b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift @@ -805,7 +805,7 @@ extension WorkspaceTests { /// Tests the unified trait system where one parent disables default traits with [] /// while another parent doesn't specify traits (defaults to default traits). /// The resulting EnabledTraitsMap should have both disablers AND enabled default traits. - func testDisablersCoexistWithDefaultTraits() async throws { + func testDefaultTraitDisablersCoexistWithDefaultTraits() async throws { let sandbox = AbsolutePath("/tmp/ws/") let fs = InMemoryFileSystem() @@ -943,4 +943,267 @@ extension WorkspaceTests { } } } + + /// Verifies that when a parent requests defaults (doesn't specify traits), + /// the defaults are properly expanded when the dependency's manifest loads. + /// The "default" trait itself should never appear in the final enabled traits list. + func testDefaultTraitSettersFlattenedOnManifestLoad() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "RootTarget", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "RootProduct", modules: ["RootTarget"])], + dependencies: [ + // Root doesn't specify traits - wants defaults + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ] + ), + ], + packages: [ + MockPackage( + name: "ChildPackage", + targets: [ + MockTarget(name: "ChildTarget"), + ], + products: [MockProduct(name: "ChildProduct", modules: ["ChildTarget"])], + traits: [ + // Default trait enables Feature1 + .init(name: "default", enabledTraits: ["Feature1"]), + .init(name: "Feature1"), + ], + versions: ["1.0.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "RootPackage") + result.check(packages: "RootPackage", "ChildPackage") + + // Verify ChildPackage has Feature1 enabled (from expanded default) + // The "default" trait should NOT appear - it should be flattened to Feature1 + result.checkPackage("ChildPackage") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on ChildPackage") + return + } + + // Should contain Feature1 (expanded from default) + XCTAssertTrue(enabledTraits.contains("Feature1")) + // Should NOT contain "default" - it should be flattened + XCTAssertFalse(enabledTraits.contains("default")) + // Should only have Feature1 + XCTAssertEqual(enabledTraits.count, 1) + } + } + } + } + + /// Verifies that when multiple parents don't specify traits (want defaults), + /// all their default requests are tracked and the result is the same regardless of load order. + /// The "default" trait should be flattened to the actual traits it enables. + func testMultipleDefaultTraitSettersOrderIndependent() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "RootTarget", + dependencies: [ + .product(name: "Parent1Product", package: "Parent1"), + .product(name: "Parent2Product", package: "Parent2"), + ] + ), + ], + products: [MockProduct(name: "RootProduct", modules: ["RootTarget"])], + dependencies: [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + ), + ], + packages: [ + MockPackage( + name: "Parent1", + targets: [ + MockTarget( + name: "Parent1Target", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "Parent1Product", modules: ["Parent1Target"])], + dependencies: [ + // Parent1 doesn't specify traits - wants defaults + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "Parent2", + targets: [ + MockTarget( + name: "Parent2Target", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "Parent2Product", modules: ["Parent2Target"])], + dependencies: [ + // Parent2 also doesn't specify traits - wants defaults + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "ChildPackage", + targets: [MockTarget(name: "ChildTarget")], + products: [MockProduct(name: "ChildProduct", modules: ["ChildTarget"])], + traits: [ + .init(name: "default", enabledTraits: ["Feature1"]), + .init(name: "Feature1"), + ], + versions: ["1.0.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.checkPackage("ChildPackage") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on ChildPackage") + return + } + + // Should have Feature1 enabled from both parents wanting defaults + XCTAssertTrue(enabledTraits.contains("Feature1")) + // Should NOT contain "default" - it should be flattened + XCTAssertFalse(enabledTraits.contains("default")) + } + } + } + } + + /// Verifies that when all parents disable traits, no defaults are enabled. + /// The final enabled traits should be empty. + func testAllParentsDisableDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "RootTarget", + dependencies: [ + .product(name: "Parent1Product", package: "Parent1"), + .product(name: "Parent2Product", package: "Parent2"), + ] + ), + ], + products: [MockProduct(name: "RootProduct", modules: ["RootTarget"])], + dependencies: [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + ), + ], + packages: [ + MockPackage( + name: "Parent1", + targets: [ + MockTarget( + name: "Parent1Target", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "Parent1Product", modules: ["Parent1Target"])], + dependencies: [ + // Parent1 disables all traits + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0"), traits: []) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "Parent2", + targets: [ + MockTarget( + name: "Parent2Target", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "Parent2Product", modules: ["Parent2Target"])], + dependencies: [ + // Parent2 also disables all traits + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0"), traits: []) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "ChildPackage", + targets: [MockTarget(name: "ChildTarget")], + products: [MockProduct(name: "ChildProduct", modules: ["ChildTarget"])], + traits: [ + .init(name: "default", enabledTraits: ["Feature1"]), + .init(name: "Feature1"), + ], + versions: ["1.0.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.checkPackage("ChildPackage") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on ChildPackage") + return + } + + // Should have NO traits enabled (both parents disabled) + XCTAssertTrue(enabledTraits.isEmpty) + } + } + } + } + } + From 02fba29c15135dbc48a235b7f7c8fb53b9a0b0f1 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Wed, 5 Nov 2025 10:54:51 -0500 Subject: [PATCH 13/14] Combined storage dictionaries into ThreadSafeBox for EnabledTraitsMap * For better handling of possible concurrent accesses into the storage of EnabledTraitsMap, since manifests are loaded in parallel, assure that the setter subscript merges the compounded modifications into an atomic action (`mutate`) * Add descriptions to remaining EnabledTraitsMap methods --- Sources/PackageModel/EnabledTrait.swift | 167 +++++++++++------- Sources/Workspace/Workspace+Traits.swift | 7 +- .../PackageModelTests/EnabledTraitTests.swift | 1 - 3 files changed, 109 insertions(+), 66 deletions(-) diff --git a/Sources/PackageModel/EnabledTrait.swift b/Sources/PackageModel/EnabledTrait.swift index 9186fe1ee85..f5692528f98 100644 --- a/Sources/PackageModel/EnabledTrait.swift +++ b/Sources/PackageModel/EnabledTrait.swift @@ -66,19 +66,34 @@ import Basics /// // The dependency has "MyTrait" trait enabled (unified from Parent2) /// print(traits[dependencyId]) // Output: ["MyTrait"] /// ``` +/// +/// ## Default Setters +/// When a parent package or trait configuration explicitly requests the`default` trait (or leaves the set of +/// traits unspecified), those setters are tracked separately. Query these using the `defaultSettersFor` subscript. public struct EnabledTraitsMap { public typealias Key = PackageIdentity public typealias Value = EnabledTraits - /// Storage for explicitly enabled traits per package. Omits packages with only the "default" trait. - private var storage: ThreadSafeKeyValueStore = .init() + private struct Storage { + /// Storage for explicitly enabled traits per package. Omits packages with only the "default" trait. + var traits: [PackageIdentity: EnabledTraits] = [:] + + /// Tracks setters that explicitly disabled default traits (via []) for each package. + var _disablers: [PackageIdentity: Set] = [:] + + /// Tracks setters that requested default traits for each package. + /// This is used when a parent doesn't specify traits, meaning it wants the dependency to use its defaults, + /// or when the `default` trait is explicitly requested. + var _defaultSetters: [PackageIdentity: Set] = [:] - /// Tracks setters that explicitly disabled default traits (via []) for each package. - private var _disablers: ThreadSafeKeyValueStore> = .init() + init() { } + + init(_ traits: [PackageIdentity: EnabledTraits]) { + self.traits = traits + } + } - /// Tracks setters that requested default traits for each package. - /// This is used when a parent doesn't specify traits, meaning it wants the dependency to use its defaults. - private var _defaultSetters: ThreadSafeKeyValueStore> = .init() + private var storage = ThreadSafeBox(Storage()) public init() { } @@ -88,89 +103,115 @@ public struct EnabledTraitsMap { } public subscript(key: PackageIdentity) -> EnabledTraits { - get { storage[key] ?? ["default"] } + get { storage.get()?.traits[key] ?? ["default"] } set { - // Omit adding "default" explicitly, since the map returns "default" - // if there are no explicit traits enabled. This will allow us to check - // for nil entries in the stored dictionary, which tells us whether - // traits have been explicitly enabled or not. - // - // However, if "default" is explicitly set by a parent (has setters), - // track it in the `defaultSetters` property. - guard !(newValue == .defaults && !newValue.isExplicitlySetDefault) else { - return - } + storage.mutate { state -> Storage? in + guard var state = state else { + return Storage() + } - // Track setter that disabled all default traits; - // continue to union existing enabled traits. - if newValue.isEmpty, let disabler = newValue.disabledBy { - if !self._disablers.contains(key) { - _disablers[key] = [] + // Omit adding "default" explicitly, since the map returns "default" + // if there are no explicit traits enabled. This will allow us to check + // for nil entries in the stored dictionary, which tells us whether + // traits have been explicitly enabled or not. + // + // However, if "default" is explicitly set by a parent (has setters), + // track it in the `defaultSetters` property. + guard !(newValue == .defaults && !newValue.isExplicitlySetDefault) else { + return state } - _disablers[key]?.insert(disabler) - } - // Check if this is an explicitly-set "default" trait (parent wants defaults enabled) - if newValue.isExplicitlySetDefault { - // Track that this parent wants default traits, but don't store the sentinel "default" - // The actual default traits will be resolved when the dependency's manifest is loaded - if let defaultSetter = newValue.first?.setters.first { - if !self._defaultSetters.contains(key) { - _defaultSetters[key] = [] + // Track default setters + if newValue.isExplicitlySetDefault { + if let defaultSetter = newValue.first?.setters.first { + state._defaultSetters[key, default: []].insert(defaultSetter) } - _defaultSetters[key]?.insert(defaultSetter) + if state.traits[key] == [] { + state.traits[key] = nil + } + return state } - // If explicitly disabled default traits prior, then - // reset this in storage and assure we can still fetch defaults. - // We will still track whenever default traits were disabled by - // keeping the _disablers map as it was. - if self.storage[key] == [] { - self.storage[key] = nil + // Track disablers + if newValue.isEmpty, let disabler = newValue.disabledBy { + state._disablers[key, default: []].insert(disabler) } - return - } - // If there are no explictly enabled traits added yet, then create entry. - if storage[key] == nil { - storage[key] = newValue - } else { - // Combine the existing set of enabled traits with the newValue. - storage[key]?.formUnion(newValue) + // Union or create; the set of enabled traits is strictly additive. + if state.traits[key] == nil { + state.traits[key] = newValue + } else { + state.traits[key]?.formUnion(newValue) + } + + return state } } } + /// Returns the set of setters that explicitly disabled default traits for a package. + /// + /// When a parent package or trait configuration sets an empty trait array (`[]`) for a package, + /// that setter is tracked as a "disabler" to record the intent to disable default traits. + /// + /// - Parameter key: The package identity to query. + /// - Returns: The set of setters that disabled default traits, or `nil` if no disablers exist. public subscript(disablersFor key: PackageIdentity) -> Set? { - get { self._disablers[key] } + storage.get()?._disablers[key] } + /// Returns the set of setters that explicitly disabled default traits for a package identified by a string. + /// + /// This is a convenience subscript that converts the string key to a `PackageIdentity`. + /// + /// - Parameter key: The package identity string to query. + /// - Returns: The set of setters that disabled default traits, or `nil` if no disablers exist. public subscript(disablersFor key: String) -> Set? { self[disablersFor: .init(key)] } + /// Returns the set of setters that requested default traits for a package. + /// + /// When a parent package or trait configuration sets default traits or leaves + /// traits unspecified, those setters are tracked. + /// + /// - Parameter key: The package identity to query. + /// - Returns: The set of setters that requested default traits, or `nil` if no default setters exist. public subscript(defaultSettersFor key: PackageIdentity) -> Set? { - get { self._defaultSetters[key] } + storage.get()?._defaultSetters[key] } + /// Returns the set of setters that requested default traits for a package identified by a string. + /// + /// This is a convenience subscript that converts the string key to a `PackageIdentity`. + /// + /// - Parameter key: The package identity string to query. + /// - Returns: The set of setters that requested default traits, or `nil` if no default setters exist. public subscript(defaultSettersFor key: String) -> Set? { self[defaultSettersFor: .init(key)] } /// Returns a list of traits that were explicitly enabled for a given package. + /// + /// - Parameter key: The package identity to query. + /// - Returns: The explicitly enabled traits, or `nil` if no traits were explicitly set (meaning the package uses defaults). public subscript(explicitlyEnabledTraitsFor key: PackageIdentity) -> EnabledTraits? { - get { storage[key] } + storage.get()?.traits[key] } /// Returns a list of traits that were explicitly enabled for a given package. + /// + /// This is a convenience subscript that converts the string key to a `PackageIdentity`. + /// + /// - Parameter key: The package identity string to query. + /// - Returns: The explicitly enabled traits, or `nil` if no traits were explicitly set (meaning the package uses defaults). public subscript(explicitlyEnabledTraitsFor key: String) -> EnabledTraits? { self[explicitlyEnabledTraitsFor: .init(key)] } - - /// Returns a dictionary literal representation of the map. + /// Returns a dictionary literal representation of the enabled traits map. public var dictionaryLiteral: [PackageIdentity: EnabledTraits] { - return storage.get() + return storage.get()?.traits ?? [:] } } @@ -178,7 +219,7 @@ public struct EnabledTraitsMap { extension EnabledTraitsMap: ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (Key, Value)...) { for (key, value) in elements { - storage[key] = value + self[key] = value } } @@ -186,11 +227,12 @@ extension EnabledTraitsMap: ExpressibleByDictionaryLiteral { let mappedDictionary = dictionary.reduce(into: [Key: Value]()) { result, element in result[PackageIdentity(element.key)] = element.value } - self.storage = .init(mappedDictionary) + + self.storage = .init(.init(mappedDictionary)) } public init(_ dictionary: [Key: Value]) { - self.storage = .init(dictionary) + self.storage = .init(.init(dictionary)) } } @@ -383,8 +425,9 @@ public struct EnabledTraits: Hashable { /// Returns true if this represents an explicitly-set "default" trait (with setters), /// as opposed to the sentinel `.defaults` value (no setters). - /// This is used to distinguish when a parent explicitly wants default traits enabled - /// versus when no traits have been specified at all. + /// This is used to distinguish when a parent package enables default traits + /// either explicitly or when no traits have been specified for a package dependency + /// at all. public var isExplicitlySetDefault: Bool { // Check if this equals .defaults (only contains "default" trait) AND has explicit setters return self == .defaults && _traits.contains(where: { !$0.setters.isEmpty }) @@ -394,6 +437,10 @@ public struct EnabledTraits: Hashable { ["default"] } + public init(_ enabledTraits: EnabledTraits) { + self._traits = enabledTraits._traits + } + private init(_ disabler: EnabledTrait.Setter) { self._disableAllTraitsSetter = disabler } @@ -407,10 +454,6 @@ public struct EnabledTraits: Hashable { self.init(enabledTraits) } - public init(_ enabledTraits: EnabledTraits) { - self._traits = enabledTraits._traits - } - private init(_ traits: C) where C.Element == EnabledTrait { self._traits = IdentifiableSet(traits) } diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift index 75fb0f4a881..856627f4fcb 100644 --- a/Sources/Workspace/Workspace+Traits.swift +++ b/Sources/Workspace/Workspace+Traits.swift @@ -33,8 +33,7 @@ extension Workspace { try manifest.enabledTraits(using: self.traitConfiguration) : self.enabledTraitsMap[manifest.packageIdentity] - let enabledTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) - self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits + var enabledTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) // Check if any parents requested default traits for this package // If so, expand the default traits and union them with existing traits @@ -49,10 +48,12 @@ extension Workspace { defaultTraits.map(\.name), setBy: setter ) - self.enabledTraitsMap[manifest.packageIdentity] = traitsFromSetter + enabledTraits.formUnion(traitsFromSetter) } } + self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits + // Check enabled traits for the dependencies for dep in manifest.dependencies { updateEnabledTraits(forDependency: dep, manifest) diff --git a/Tests/PackageModelTests/EnabledTraitTests.swift b/Tests/PackageModelTests/EnabledTraitTests.swift index 9c58c0c6f3e..35b8a5fc7f7 100644 --- a/Tests/PackageModelTests/EnabledTraitTests.swift +++ b/Tests/PackageModelTests/EnabledTraitTests.swift @@ -14,7 +14,6 @@ import Testing import struct PackageModel.EnabledTrait import struct PackageModel.EnabledTraits import struct PackageModel.EnabledTraitsMap -//import struct PackageModel.PackageIdentity @Suite( .tags( From f4826ebeb23abd5c1a8cce497fc9191f26eed0fa Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Wed, 5 Nov 2025 10:59:46 -0500 Subject: [PATCH 14/14] Cleanup --- Sources/Workspace/Workspace+Dependencies.swift | 9 --------- Tests/PackageModelTests/ManifestTests.swift | 1 - 2 files changed, 10 deletions(-) diff --git a/Sources/Workspace/Workspace+Dependencies.swift b/Sources/Workspace/Workspace+Dependencies.swift index 23f327133d6..743e6ac926e 100644 --- a/Sources/Workspace/Workspace+Dependencies.swift +++ b/Sources/Workspace/Workspace+Dependencies.swift @@ -515,15 +515,6 @@ extension Workspace { let rootManifestsMinimumToolsVersion = rootManifests.values.map(\.toolsVersion).min() ?? ToolsVersion.current let resolvedFileOriginHash = try self.computeResolvedFileOriginHash(root: root) - // Precompute enabled traits, beginning with - // root manifests, if we haven't already done so. -// if self.enabledTraitsMap.dictionaryLiteral.isEmpty { -// let rootManifestMap = rootManifests.values.reduce(into: [PackageIdentity: Manifest]()) { manifestMap, manifest in -// manifestMap[manifest.packageIdentity] = manifest -// } -// self.enabledTraitsMap = .init(try precomputeTraits(rootManifests.values.map({ $0 }), rootManifestMap)) -// } - // Load the current manifests. let graphRoot = try PackageGraphRoot( input: root, diff --git a/Tests/PackageModelTests/ManifestTests.swift b/Tests/PackageModelTests/ManifestTests.swift index 5f9ba02d81c..0a0b6195a22 100644 --- a/Tests/PackageModelTests/ManifestTests.swift +++ b/Tests/PackageModelTests/ManifestTests.swift @@ -465,7 +465,6 @@ class ManifestTests: XCTestCase { let enabledTraits = EnabledTraits(["Trait1"], setBy: .trait("default")) for trait in traits.sorted(by: { $0.name < $1.name }) { - // TODO bp i think default is getting failed here XCTAssertTrue(try manifest.isTraitEnabled(trait, enabledTraits)) } }