Skip to content

Commit 60b4913

Browse files
authored
[Traits] Disallow disabling default traits of a package without traits (#8326)
# Motivation Traits are a great way for package authors to offer customization of the functionality they provide. However, moving existing API behind a trait is considered an API breaking change since packages that depend on them might have disabled all default traits. This makes it almost impossible for existing packages to adopt traits for existing code. # Modifications This PR disallows disabling the default traits for packages with no traits at all. This allows package authors to move existing API behind traits once since no consumer can disable the default traits before. # Result With this change we create a migration path for existing packages to traits without them breaking their APIs.
1 parent a45c401 commit 60b4913

File tree

9 files changed

+91
-6
lines changed

9 files changed

+91
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// swift-tools-version: 6.1
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "DisablingEmptyDefaultsExample",
7+
dependencies: [
8+
.package(
9+
path: "../Package11",
10+
traits: []
11+
),
12+
],
13+
targets: [
14+
.executableTarget(
15+
name: "DisablingEmptyDefaultsExample"
16+
),
17+
]
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@main
2+
struct Example {
3+
static func main() {
4+
5+
}
6+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// swift-tools-version: 6.1
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "Package11",
7+
products: [
8+
.library(
9+
name: "Package11Library1",
10+
targets: ["Package11Library1"]
11+
),
12+
],
13+
targets: [
14+
.target(
15+
name: "Package11Library1"
16+
),
17+
]
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public func hello() {
2+
print("Package11Library1")
3+
}

Sources/PackageGraph/ModulesGraph+Loading.swift

+12
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ extension ModulesGraph {
112112
manifest: package.manifest,
113113
productFilter: .everything,
114114
enabledTraits: calculateEnabledTraits(
115+
parentPackage: nil,
115116
identity: identity,
116117
manifest: package.manifest,
117118
explictlyEnabledTraits: enabledTraits
@@ -154,6 +155,7 @@ extension ModulesGraph {
154155
manifest: manifest,
155156
productFilter: dependency.productFilter,
156157
enabledTraits: calculateEnabledTraits(
158+
parentPackage: node.item.identity,
157159
identity: dependency.identity,
158160
manifest: manifest,
159161
explictlyEnabledTraits: explictlyEnabledTraits.flatMap { Set($0) }
@@ -941,6 +943,7 @@ private func emitDuplicateProductDiagnostic(
941943
}
942944

943945
private func calculateEnabledTraits(
946+
parentPackage: PackageIdentity?,
944947
identity: PackageIdentity,
945948
manifest: Manifest,
946949
explictlyEnabledTraits: Set<String>?
@@ -958,6 +961,15 @@ private func calculateEnabledTraits(
958961
throw ModuleError.invalidTrait(package: identity, trait: trait)
959962
}
960963
}
964+
965+
if let parentPackage, !(explictlyEnabledTraits == nil || areDefaultsEnabled) && manifest.traits.isEmpty {
966+
// We throw an error when default traits are disabled for a package without any traits
967+
// This allows packages to initially move new API behind traits once.
968+
throw ModuleError.disablingDefaultTraitsOnEmptyTraits(
969+
parentPackage: parentPackage,
970+
packageName: manifest.displayName
971+
)
972+
}
961973

962974
// We have to enable all default traits if no traits are enabled or the defaults are explicitly enabled
963975
if explictlyEnabledTraits == nil || areDefaultsEnabled {

Sources/PackageLoading/PackageBuilder.swift

+9
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ public enum ModuleError: Swift.Error {
9797
package: PackageIdentity,
9898
trait: String
9999
)
100+
101+
case disablingDefaultTraitsOnEmptyTraits(
102+
parentPackage: PackageIdentity,
103+
packageName: String
104+
)
100105
}
101106

102107
extension ModuleError: CustomStringConvertible {
@@ -179,6 +184,10 @@ extension ModuleError: CustomStringConvertible {
179184
return """
180185
Trait '"\(trait)"' is not declared by package '\(package)'.
181186
"""
187+
case .disablingDefaultTraitsOnEmptyTraits(let parentPackage, let packageName):
188+
return """
189+
Disabled default traits by package '\(parentPackage)' on package '\(packageName)' that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break.
190+
"""
182191
}
183192
}
184193
}

Sources/_InternalTestSupport/ManifestExtensions.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extension Manifest {
3232
dependencies: [PackageDependency] = [],
3333
products: [ProductDescription] = [],
3434
targets: [TargetDescription] = [],
35-
traits: Set<TraitDescription> = []
35+
traits: Set<TraitDescription> = [.init(name: "defaults")]
3636
) -> Manifest {
3737
Self.createManifest(
3838
displayName: displayName,
@@ -70,7 +70,7 @@ extension Manifest {
7070
dependencies: [PackageDependency] = [],
7171
products: [ProductDescription] = [],
7272
targets: [TargetDescription] = [],
73-
traits: Set<TraitDescription> = []
73+
traits: Set<TraitDescription> = [.init(name: "defaults")]
7474
) -> Manifest {
7575
Self.createManifest(
7676
displayName: displayName,
@@ -220,7 +220,7 @@ extension Manifest {
220220
dependencies: [PackageDependency] = [],
221221
products: [ProductDescription] = [],
222222
targets: [TargetDescription] = [],
223-
traits: Set<TraitDescription> = []
223+
traits: Set<TraitDescription> = [.init(name: "defaults")]
224224
) -> Manifest {
225225
return Manifest(
226226
displayName: displayName,

Sources/_InternalTestSupport/MockPackage.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public struct MockPackage {
3535
targets: [MockTarget],
3636
products: [MockProduct] = [],
3737
dependencies: [MockDependency] = [],
38-
traits: Set<TraitDescription> = [],
38+
traits: Set<TraitDescription> = [.init(name: "defaults")],
3939
versions: [String?] = [],
4040
revisionProvider: ((String) -> String)? = nil,
4141
toolsVersion: ToolsVersion? = nil
@@ -60,7 +60,7 @@ public struct MockPackage {
6060
targets: [MockTarget],
6161
products: [MockProduct],
6262
dependencies: [MockDependency] = [],
63-
traits: Set<TraitDescription> = [],
63+
traits: Set<TraitDescription> = [.init(name: "defaults")],
6464
versions: [String?] = [],
6565
revisionProvider: ((String) -> String)? = nil,
6666
toolsVersion: ToolsVersion? = nil
@@ -86,7 +86,7 @@ public struct MockPackage {
8686
targets: [MockTarget],
8787
products: [MockProduct],
8888
dependencies: [MockDependency] = [],
89-
traits: Set<TraitDescription> = [],
89+
traits: Set<TraitDescription> = [.init(name: "defaults")],
9090
versions: [String?] = [],
9191
revisionProvider: ((String) -> String)? = nil,
9292
toolsVersion: ToolsVersion? = nil

Tests/FunctionalTests/TraitTests.swift

+19
Original file line numberDiff line numberDiff line change
@@ -264,5 +264,24 @@ final class TraitTests: XCTestCase {
264264
XCTAssertTrue(symbolGraph.contains("TypeGatedByPackage10Trait2"))
265265
}
266266
}
267+
268+
func testPackageDisablinDefaultsTrait_whenNoTraits() async throws {
269+
try await fixture(name: "Traits") { fixturePath in
270+
do {
271+
let (_, _) = try await executeSwiftRun(fixturePath.appending("DisablingEmptyDefaultsExample"), "DisablingEmptyDefaultsExample")
272+
} catch let error as SwiftPMError {
273+
switch error {
274+
case .packagePathNotFound:
275+
throw error
276+
case .executionFailure(_, _, let stderr):
277+
let expectedErr = """
278+
error: Disabled default traits by package 'disablingemptydefaultsexample' on package 'Package11' that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break.
279+
280+
"""
281+
XCTAssertTrue(stderr.contains(expectedErr))
282+
}
283+
}
284+
}
285+
}
267286
}
268287
#endif

0 commit comments

Comments
 (0)