Skip to content

Commit abd3157

Browse files
authored
Fix for .when(traits:) condition not working for multiple traits (#9015)
The culprit here is the fact that we were essentially doing an AND boolean check across all the traits in the target dependency condition, when we should be doing an OR boolean check. If at least one trait is enabled in the list of traits in the target dependency condition, then the target dependecy should be considered enabled as well.
1 parent 6f3360b commit abd3157

File tree

9 files changed

+300
-12
lines changed

9 files changed

+300
-12
lines changed

Fixtures/Traits/Example/Package.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ let package = Package(
2525
"BuildCondition1",
2626
"BuildCondition2",
2727
"BuildCondition3",
28+
"ExtraTrait",
2829
],
2930
dependencies: [
3031
.package(
@@ -101,6 +102,11 @@ let package = Package(
101102
package: "Package10",
102103
condition: .when(traits: ["Package10"])
103104
),
105+
.product(
106+
name: "Package10Library2",
107+
package: "Package10",
108+
condition: .when(traits: ["Package10", "ExtraTrait"])
109+
)
104110
],
105111
swiftSettings: [
106112
.define("DEFINE1", .when(traits: ["BuildCondition1"])),

Fixtures/Traits/Example/Sources/Example/Example.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import Package9Library1
2121
#endif
2222
#if Package10
2323
import Package10Library1
24+
import Package10Library2
25+
#endif
26+
#if ExtraTrait
27+
import Package10Library2
2428
#endif
2529

2630
@main
@@ -49,6 +53,10 @@ struct Example {
4953
#endif
5054
#if Package10
5155
Package10Library1.hello()
56+
Package10Library2.hello()
57+
#endif
58+
#if ExtraTrait
59+
Package10Library2.hello()
5260
#endif
5361
#if DEFINE1
5462
print("DEFINE1 enabled")

Fixtures/Traits/Package10/Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ let package = Package(
99
name: "Package10Library1",
1010
targets: ["Package10Library1"]
1111
),
12+
.library(
13+
name: "Package10Library2",
14+
targets: ["Package10Library2"]
15+
),
1216
],
1317
traits: [
1418
"Package10Trait1",
@@ -18,6 +22,9 @@ let package = Package(
1822
.target(
1923
name: "Package10Library1"
2024
),
25+
.target(
26+
name: "Package10Library2"
27+
),
2128
.plugin(
2229
name: "SymbolGraphExtract",
2330
capability: .command(
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public func hello() {
2+
print("Package10Library2 has been included.")
3+
}

Sources/PackageModel/Manifest/Manifest+Traits.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ extension Manifest {
9595
_ parentPackage: PackageIdentifier? = nil
9696
) throws {
9797
guard supportsTraits else {
98-
if explicitlyEnabledTraits != ["default"] /*!explicitlyEnabledTraits.contains("default")*/ {
98+
if explicitlyEnabledTraits != ["default"] {
9999
throw TraitError.traitsNotSupported(
100100
parent: parentPackage,
101101
package: .init(self),
@@ -116,7 +116,7 @@ extension Manifest {
116116
let areDefaultsEnabled = enabledTraits.contains("default")
117117

118118
// Ensure that disabling default traits is disallowed for packages that don't define any traits.
119-
if !(explicitlyEnabledTraits == nil || areDefaultsEnabled) && !self.supportsTraits {
119+
if !areDefaultsEnabled && !self.supportsTraits {
120120
// We throw an error when default traits are disabled for a package without any traits
121121
// This allows packages to initially move new API behind traits once.
122122
throw TraitError.traitsNotSupported(
@@ -449,15 +449,18 @@ extension Manifest {
449449

450450
let traitsToEnable = self.traitGuardedTargetDependencies(for: target)[dependency] ?? []
451451

452-
let isEnabled = try traitsToEnable.allSatisfy { try self.isTraitEnabled(
452+
// Check if any of the traits guarding this dependency is enabled;
453+
// if so, the condition is met and the target dependency is considered
454+
// to be in an enabled state.
455+
let isEnabled = try traitsToEnable.contains(where: { try self.isTraitEnabled(
453456
.init(stringLiteral: $0),
454457
enabledTraits,
455-
) }
458+
) })
456459

457460
return traitsToEnable.isEmpty || isEnabled
458461
}
459462
/// Determines whether a given package dependency is used by this manifest given a set of enabled traits.
460-
public func isPackageDependencyUsed(_ dependency: PackageDependency, enabledTraits: Set<String>/* = ["default"]*/) throws -> Bool {
463+
public func isPackageDependencyUsed(_ dependency: PackageDependency, enabledTraits: Set<String>) throws -> Bool {
461464
if self.pruneDependencies {
462465
let usedDependencies = try self.usedDependencies(withTraits: enabledTraits)
463466
let foundKnownPackage = usedDependencies.knownPackage.contains(where: {
@@ -478,8 +481,8 @@ extension Manifest {
478481

479482
// if target deps is empty, default to returning true here.
480483
let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({
481-
let condition = $0.subtracting(enabledTraits)
482-
return !condition.isEmpty
484+
let isGuarded = $0.intersection(enabledTraits).isEmpty
485+
return isGuarded
483486
})
484487

485488
let isUsedWithoutTraitGuarding = !targetDependenciesForPackageDependency.filter({ $0.condition?.traits == nil }).isEmpty
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct SPMBuildCore.BuildSystemProvider
14+
import enum PackageModel.BuildConfiguration
15+
16+
/// A utility struct that represents a list of traits that would be passed through the command line.
17+
/// This is used for testing purposes, and its use is currently specific to the `TraitTests.swift`
18+
public struct TraitArgumentData {
19+
public var traitsArgument: String
20+
public var expectedOutput: String
21+
}
22+
23+
public func getTraitCombinations(_ traitsAndMessage: (traits: String, output: String)...) -> [TraitArgumentData] {
24+
traitsAndMessage.map { traitListAndMessage in
25+
TraitArgumentData(traitsArgument: traitListAndMessage.traits, expectedOutput: traitListAndMessage.output)
26+
}
27+
}

Tests/FunctionalTests/TraitTests.swift

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ struct TraitTests {
111111
Package10Library1 trait2 enabled
112112
Package10Library1 trait1 enabled
113113
Package10Library1 trait2 enabled
114+
Package10Library2 has been included.
114115
DEFINE1 enabled
115116
DEFINE2 disabled
116117
DEFINE3 disabled
@@ -362,6 +363,8 @@ struct TraitTests {
362363
Package10Library1 trait2 enabled
363364
Package10Library1 trait1 enabled
364365
Package10Library1 trait2 enabled
366+
Package10Library2 has been included.
367+
Package10Library2 has been included.
365368
DEFINE1 enabled
366369
DEFINE2 enabled
367370
DEFINE3 enabled
@@ -421,6 +424,8 @@ struct TraitTests {
421424
Package10Library1 trait2 enabled
422425
Package10Library1 trait1 enabled
423426
Package10Library1 trait2 enabled
427+
Package10Library2 has been included.
428+
Package10Library2 has been included.
424429
DEFINE1 enabled
425430
DEFINE2 enabled
426431
DEFINE3 enabled
@@ -454,7 +459,7 @@ struct TraitTests {
454459
let json = try JSON(bytes: ByteString(encodingAsUTF8: dumpOutput))
455460
guard case .dictionary(let contents) = json else { Issue.record("unexpected result"); return }
456461
guard case .array(let traits)? = contents["traits"] else { Issue.record("unexpected result"); return }
457-
#expect(traits.count == 12)
462+
#expect(traits.count == 13)
458463
}
459464
}
460465

@@ -653,4 +658,78 @@ struct TraitTests {
653658
}
654659
}
655660
}
661+
662+
@Test(
663+
.IssueSwiftBuildLinuxRunnable,
664+
.IssueProductTypeForObjectLibraries,
665+
.tags(
666+
Tag.Feature.Command.Run,
667+
),
668+
arguments:
669+
getBuildData(for: SupportedBuildSystemOnAllPlatforms),
670+
getTraitCombinations(
671+
("ExtraTrait",
672+
"""
673+
Package10Library2 has been included.
674+
DEFINE1 disabled
675+
DEFINE2 disabled
676+
DEFINE3 disabled
677+
678+
"""
679+
),
680+
("Package10",
681+
"""
682+
Package10Library1 trait1 disabled
683+
Package10Library1 trait2 enabled
684+
Package10Library2 has been included.
685+
DEFINE1 disabled
686+
DEFINE2 disabled
687+
DEFINE3 disabled
688+
689+
"""
690+
),
691+
("ExtraTrait,Package10",
692+
"""
693+
Package10Library1 trait1 disabled
694+
Package10Library1 trait2 enabled
695+
Package10Library2 has been included.
696+
Package10Library2 has been included.
697+
DEFINE1 disabled
698+
DEFINE2 disabled
699+
DEFINE3 disabled
700+
701+
"""
702+
)
703+
)
704+
)
705+
func traits_whenManyTraitsEnableTargetDependency(
706+
data: BuildData,
707+
traits: TraitArgumentData
708+
) async throws {
709+
try await withKnownIssue(
710+
"""
711+
Linux: https://github.com/swiftlang/swift-package-manager/issues/8416,
712+
Windows: https://github.com/swiftlang/swift-build/issues/609
713+
""",
714+
isIntermittent: (ProcessInfo.hostOperatingSystem == .windows),
715+
) {
716+
try await fixture(name: "Traits") { fixturePath in
717+
// We expect no warnings to be produced. Specifically no unused dependency warnings.
718+
let unusedDependencyRegex = try Regex("warning: '.*': dependency '.*' is not used by any target")
719+
720+
let (stdout, stderr) = try await executeSwiftRun(
721+
fixturePath.appending("Example"),
722+
"Example",
723+
configuration: data.config,
724+
extraArgs: ["--traits", traits.traitsArgument],
725+
buildSystem: data.buildSystem,
726+
)
727+
#expect(!stderr.contains(unusedDependencyRegex))
728+
#expect(stdout == traits.expectedOutput)
729+
}
730+
} when: {
731+
(ProcessInfo.hostOperatingSystem == .windows && (CiEnvironment.runningInSmokeTestPipeline || data.buildSystem == .swiftbuild))
732+
|| (data.buildSystem == .swiftbuild && ProcessInfo.hostOperatingSystem == .linux && CiEnvironment.runningInSelfHostedPipeline)
733+
}
734+
}
656735
}

Tests/PackageModelTests/ManifestTests.swift

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ class ManifestTests: XCTestCase {
683683
let dependencies: [PackageDependency] = [
684684
.localSourceControl(path: "/Bar", requirement: .upToNextMajor(from: "1.0.0")),
685685
.localSourceControl(path: "/Baz", requirement: .upToNextMajor(from: "1.0.0")),
686+
.localSourceControl(path: "/Cosmic", requirement: .upToNextMajor(from: "1.0.0")),
686687
]
687688

688689
let products = try [
@@ -711,13 +712,20 @@ class ManifestTests: XCTestCase {
711712
package: "Boom"
712713
)
713714

715+
let manyTraitsEnableTargetDependency: TargetDescription.Dependency = .product(
716+
name: "Supernova",
717+
package: "Cosmic",
718+
condition: .init(traits: ["Space", "Music"])
719+
)
720+
714721
let target = try TargetDescription(
715722
name: "Foo",
716723
dependencies: [
717724
unguardedTargetDependency,
718725
trait3GuardedTargetDependency,
719726
defaultTraitGuardedTargetDependency,
720727
enabledTargetDependencyWithSamePackage,
728+
manyTraitsEnableTargetDependency
721729
]
722730
)
723731

@@ -726,6 +734,8 @@ class ManifestTests: XCTestCase {
726734
TraitDescription(name: "Trait1", enabledTraits: ["Trait2"]),
727735
TraitDescription(name: "Trait2"),
728736
TraitDescription(name: "Trait3"),
737+
TraitDescription(name: "Space"),
738+
TraitDescription(name: "Music"),
729739
]
730740

731741
do {
@@ -792,18 +802,47 @@ class ManifestTests: XCTestCase {
792802
enabledTargetDependencyWithSamePackage,
793803
enabledTraits: []
794804
))
805+
806+
// Test variations of traits that enable a target dependency that is unguarded by many traits.
807+
XCTAssertFalse(try manifest.isTargetDependencyEnabled(
808+
target: "Foo",
809+
manyTraitsEnableTargetDependency,
810+
enabledTraits: []
811+
))
812+
XCTAssertTrue(try manifest.isTargetDependencyEnabled(
813+
target: "Foo",
814+
manyTraitsEnableTargetDependency,
815+
enabledTraits: ["Space"]
816+
))
817+
XCTAssertTrue(try manifest.isTargetDependencyEnabled(
818+
target: "Foo",
819+
manyTraitsEnableTargetDependency,
820+
enabledTraits: ["Music"]
821+
))
822+
XCTAssertTrue(try manifest.isTargetDependencyEnabled(
823+
target: "Foo",
824+
manyTraitsEnableTargetDependency,
825+
enabledTraits: ["Music", "Space"]
826+
))
827+
XCTAssertTrue(try manifest.isTargetDependencyEnabled(
828+
target: "Foo",
829+
manyTraitsEnableTargetDependency,
830+
enabledTraits: ["Trait3", "Music", "Space", "Trait1", "Trait2"]
831+
))
795832
}
796833
}
797834

798835
func testIsPackageDependencyUsed() throws {
799836
let bar: PackageDependency = .localSourceControl(path: "/Bar", requirement: .upToNextMajor(from: "1.0.0"))
800837
let baz: PackageDependency = .localSourceControl(path: "/Baz", requirement: .upToNextMajor(from: "1.0.0"))
801838
let bam: PackageDependency = .localSourceControl(path: "/Bam", requirement: .upToNextMajor(from: "1.0.0"))
839+
let drinks: PackageDependency = .localSourceControl(path: "/Drinks", requirement: .upToNextMajor(from: "1.0.0"))
802840

803841
let dependencies: [PackageDependency] = [
804842
bar,
805843
baz,
806844
bam,
845+
drinks,
807846
]
808847

809848
let products = try [
@@ -832,12 +871,19 @@ class ManifestTests: XCTestCase {
832871
package: "Bam"
833872
)
834873

874+
let manyTraitsGuardingTargetDependency: TargetDescription.Dependency = .product(
875+
name: "Coffee",
876+
package: "Drinks",
877+
condition: .init(traits: ["Sugar", "Cream"])
878+
)
879+
835880
let target = try TargetDescription(
836881
name: "Foo",
837882
dependencies: [
838883
unguardedTargetDependency,
839884
trait3GuardedTargetDependency,
840885
defaultTraitGuardedTargetDependency,
886+
manyTraitsGuardingTargetDependency
841887
]
842888
)
843889

@@ -856,6 +902,8 @@ class ManifestTests: XCTestCase {
856902
TraitDescription(name: "Trait1", enabledTraits: ["Trait2"]),
857903
TraitDescription(name: "Trait2"),
858904
TraitDescription(name: "Trait3"),
905+
TraitDescription(name: "Sugar"),
906+
TraitDescription(name: "Cream"),
859907
]
860908

861909
do {
@@ -879,19 +927,19 @@ class ManifestTests: XCTestCase {
879927
traits: traits
880928
)
881929

882-
// XCTAssertTrue(try manifest.isPackageDependencyUsed(bar))
883930
XCTAssertTrue(try manifest.isPackageDependencyUsed(bar, enabledTraits: []))
884-
// XCTAssertFalse(try manifest.isPackageDependencyUsed(baz))
885931
XCTAssertTrue(try manifest.isPackageDependencyUsed(baz, enabledTraits: ["Trait3"]))
886-
// XCTAssertTrue(try manifest.isPackageDependencyUsed(bam))
887932
XCTAssertFalse(try manifest.isPackageDependencyUsed(bam, enabledTraits: []))
888933
XCTAssertFalse(try manifest.isPackageDependencyUsed(bam, enabledTraits: ["Trait3"]))
934+
XCTAssertFalse(try manifest.isPackageDependencyUsed(drinks, enabledTraits: []))
935+
XCTAssertTrue(try manifest.isPackageDependencyUsed(drinks, enabledTraits: ["Sugar"]))
936+
XCTAssertTrue(try manifest.isPackageDependencyUsed(drinks, enabledTraits: ["Cream"]))
937+
XCTAssertTrue(try manifest.isPackageDependencyUsed(drinks, enabledTraits: ["Sugar", "Cream"]))
889938

890939
// Configuration of the manifest that includes a case where there exists a target
891940
// dependency that depends on the same package as another target dependency, but
892941
// is unguarded by traits; therefore, this package dependency should be considered used
893942
// in every scenario, regardless of the passed trait configuration.
894-
// XCTAssertTrue(try manifestWithBamDependencyAlwaysUsed.isPackageDependencyUsed(bam, enabledTraits: nil))
895943
XCTAssertTrue(try manifestWithBamDependencyAlwaysUsed.isPackageDependencyUsed(bam, enabledTraits: []))
896944
XCTAssertTrue(try manifestWithBamDependencyAlwaysUsed.isPackageDependencyUsed(bam, enabledTraits: ["Trait3"]))
897945
}

0 commit comments

Comments
 (0)