Skip to content

Commit 3cdae8e

Browse files
authored
Add support for traits in manifest code generation (#9173)
Add support for traits in manifest code generation ### Motivation: Currently, manifest code generation does not support the trait feature. This results in incomplete or incorrect generated manifests whenever traits are involved. To ensure manifests accurately reflect the package definition, trait-related constructs need to be supported. ### Modifications: This patch implements trait support in manifest code generation. Specifically, it adds support for three syntactic forms: - Defining package traits - Declaring trait conditions on package dependencies - Declaring trait conditions on target dependencies ### Result: Manifest code generation will correctly emit trait-related constructs. Packages that define traits, or that depend on other packages/targets conditionally based on traits, will now be represented accurately in the generated manifest.
1 parent 4133d76 commit 3cdae8e

File tree

4 files changed

+218
-3
lines changed

4 files changed

+218
-3
lines changed

Sources/PackageModel/Manifest/PackageDependencyDescription.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public enum PackageDependency: Equatable, Hashable, Sendable {
2424
/// A condition that limits the application of a dependencies trait.
2525
package struct Condition: Hashable, Sendable, Codable {
2626
/// The set of traits of this package that enable the dependency's trait.
27-
private let traits: Set<String>?
27+
package let traits: Set<String>?
2828

2929
public init(traits: Set<String>?) {
3030
self.traits = traits
@@ -73,6 +73,11 @@ public enum PackageDependency: Equatable, Hashable, Sendable {
7373
condition: condition
7474
)
7575
}
76+
77+
// represents `.defaults`
78+
public var isDefaultsCase: Bool {
79+
name == "default" && condition == nil
80+
}
7681
}
7782

7883
case fileSystem(FileSystem)

Sources/PackageModel/ManifestSourceGeneration.swift

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ fileprivate extension SourceCodeFragment {
102102
params.append(SourceCodeFragment(key: "products", subnodes: nodes))
103103
}
104104

105+
if !manifest.traits.isEmpty {
106+
let nodes = manifest.traits.map { SourceCodeFragment(from: $0) }
107+
params.append(SourceCodeFragment(key: "traits", subnodes: nodes))
108+
}
109+
105110
if !manifest.dependencies.isEmpty {
106111
let nodes = manifest.dependencies.map{ SourceCodeFragment(from: $0, pathAnchor: packageDirectory) }
107112
params.append(SourceCodeFragment(key: "dependencies", subnodes: nodes))
@@ -192,9 +197,68 @@ fileprivate extension SourceCodeFragment {
192197
params.append(SourceCodeFragment("\"\(range.lowerBound)\"..<\"\(range.upperBound)\""))
193198
}
194199
}
200+
201+
if let traits = dependency.traits {
202+
// If only `.defaults` is specified, do not output `traits:` .
203+
// This is because `traits:` is not available in toolchains earlier than 6.1.
204+
let isDefault = traits.count == 1 &&
205+
traits.allSatisfy(\.isDefaultsCase)
206+
207+
if !isDefault {
208+
let traits = traits.sorted { a, b in
209+
PackageDependency.Trait.precedes(a, b)
210+
}
211+
params.append(
212+
SourceCodeFragment(
213+
key: "traits",
214+
subnodes: traits.map { SourceCodeFragment(from: $0) }
215+
)
216+
)
217+
}
218+
}
219+
195220
self.init(enum: "package", subnodes: params)
196221
}
197-
222+
223+
init(from trait: PackageDependency.Trait) {
224+
if trait.isDefaultsCase {
225+
self.init(enum: "defaults")
226+
return
227+
}
228+
229+
guard let condition = trait.condition else {
230+
self.init(string: trait.name)
231+
return
232+
}
233+
234+
let conditionNode = SourceCodeFragment(
235+
key: "condition",
236+
subnode: SourceCodeFragment(from: condition)
237+
)
238+
239+
self.init(enum: "trait", subnodes: [
240+
SourceCodeFragment(key: "name", string: trait.name),
241+
conditionNode
242+
])
243+
}
244+
245+
init(from condition: PackageDependency.Trait.Condition) {
246+
var params: [SourceCodeFragment] = []
247+
248+
if let trait = condition.traits {
249+
params.append(
250+
SourceCodeFragment(
251+
key: "traits",
252+
subnodes: trait.sorted().map {
253+
SourceCodeFragment(string: $0)
254+
}
255+
)
256+
)
257+
}
258+
259+
self.init(enum: "when", subnodes: params)
260+
}
261+
198262
/// Instantiates a SourceCodeFragment to represent a single product. If there's a custom product generator, it gets
199263
/// a chance to generate the source code fragments before checking the default types.
200264
init(from product: ProductDescription, customProductTypeSourceGenerator: ManifestCustomProductTypeSourceGenerator?, toolsVersion: ToolsVersion) rethrows {
@@ -261,6 +325,41 @@ fileprivate extension SourceCodeFragment {
261325
}
262326
}
263327

328+
init(from trait: TraitDescription) {
329+
let enabledTraitsNode = SourceCodeFragment(
330+
key: "enabledTraits",
331+
subnodes: trait.enabledTraits.sorted().map {
332+
SourceCodeFragment(string: $0)
333+
}
334+
)
335+
336+
if trait.isDefault {
337+
self.init(enum: "default", subnodes: [enabledTraitsNode])
338+
return
339+
}
340+
341+
if trait.description == nil, trait.enabledTraits.isEmpty {
342+
self.init(string: trait.name)
343+
return
344+
}
345+
346+
var params: [SourceCodeFragment] = [
347+
SourceCodeFragment(key: "name", string: trait.name)
348+
]
349+
350+
if let description = trait.description {
351+
params.append(
352+
SourceCodeFragment(key: "description", string: description)
353+
)
354+
}
355+
356+
if !trait.enabledTraits.isEmpty {
357+
params.append(enabledTraitsNode)
358+
}
359+
360+
self.init(enum: "trait", subnodes: params)
361+
}
362+
264363
/// Instantiates a SourceCodeFragment to represent a single target.
265364
init(from target: TargetDescription) {
266365
var params: [SourceCodeFragment] = []
@@ -428,6 +527,13 @@ fileprivate extension SourceCodeFragment {
428527
if let configName = condition.config {
429528
params.append(SourceCodeFragment(key: "configuration", enum: configName))
430529
}
530+
if let traits = condition.traits {
531+
params.append(
532+
SourceCodeFragment(key: "traits", subnodes: traits.sorted().map { trait in
533+
SourceCodeFragment(string: trait)
534+
})
535+
)
536+
}
431537
self.init(enum: "when", subnodes: params)
432538
}
433539

@@ -1035,6 +1141,47 @@ public struct SourceCodeFragment {
10351141
}
10361142
}
10371143

1144+
extension Optional {
1145+
fileprivate static func precedes(
1146+
_ a: Wrapped?, _ b: Wrapped?,
1147+
compareWrapped: (Wrapped, Wrapped) -> Bool
1148+
) -> Bool {
1149+
switch (a, b) {
1150+
case (.none, .none): return false
1151+
case (.none, .some): return true
1152+
case (.some, .none): return false
1153+
case (.some(let a), .some(let b)):
1154+
return compareWrapped(a, b)
1155+
}
1156+
}
1157+
}
1158+
1159+
extension PackageDependency.Trait {
1160+
fileprivate static func precedes(_ a: PackageDependency.Trait, _ b: PackageDependency.Trait) -> Bool {
1161+
if a.name != b.name { return a.name < b.name }
1162+
1163+
if a.condition != b.condition {
1164+
return Optional.precedes(a.condition, b.condition) { a, b in
1165+
PackageDependency.Trait.Condition.precedes(a, b)
1166+
}
1167+
}
1168+
1169+
return false
1170+
}
1171+
}
1172+
1173+
extension PackageDependency.Trait.Condition {
1174+
fileprivate static func precedes(_ a: PackageDependency.Trait.Condition, _ b: PackageDependency.Trait.Condition) -> Bool {
1175+
if a.traits != b.traits {
1176+
return Optional.precedes(a.traits, b.traits) { a, b in
1177+
a.sorted().lexicographicallyPrecedes(b.sorted())
1178+
}
1179+
}
1180+
1181+
return false
1182+
}
1183+
}
1184+
10381185
extension TargetBuildSettingDescription.Kind {
10391186
fileprivate var name: String {
10401187
switch self {

Sources/_InternalTestSupport/XCTAssertHelpers.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,19 @@ public func XCTRequires(
123123
}
124124
}
125125

126+
public func XCTSkipIfCompilerLessThan6_1() throws {
127+
swiftTestingTestCalledAnXCTestAPI()
128+
#if compiler(>=6.1)
129+
#else
130+
throw XCTSkip("Skipping as compiler version is less than 6.1")
131+
#endif
132+
}
133+
126134
public func XCTSkipIfCompilerLessThan6_2() throws {
127135
swiftTestingTestCalledAnXCTestAPI()
128136
#if compiler(>=6.2)
129137
#else
130-
throw XCTSkip("Skipping as compiler version is less thann 6.2")
138+
throw XCTSkip("Skipping as compiler version is less than 6.2")
131139
#endif
132140
}
133141

Tests/WorkspaceTests/ManifestSourceGenerationTests.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ final class ManifestSourceGenerationTests: XCTestCase {
107107
XCTAssertEqual(newManifest.pkgConfig, manifest.pkgConfig, "pkgConfig not as expected" + failureDetails, file: file, line: line)
108108
XCTAssertEqual(newManifest.providers, manifest.providers, "providers not as expected" + failureDetails, file: file, line: line)
109109
XCTAssertEqual(newManifest.products, manifest.products, "products not as expected" + failureDetails, file: file, line: line)
110+
XCTAssertEqual(newManifest.traits, manifest.traits, "traits not as expected" + failureDetails, file: file, line: line)
110111
XCTAssertEqual(newManifest.dependencies, manifest.dependencies, "dependencies not as expected" + failureDetails, file: file, line: line)
111112
XCTAssertEqual(newManifest.targets, manifest.targets, "targets not as expected" + failureDetails, file: file, line: line)
112113
XCTAssertEqual(newManifest.swiftLanguageVersions, manifest.swiftLanguageVersions, "swiftLanguageVersions not as expected" + failureDetails, file: file, line: line)
@@ -949,4 +950,58 @@ final class ManifestSourceGenerationTests: XCTestCase {
949950
let contents = try manifest.generateManifestFileContents(packageDirectory: manifest.path.parentDirectory)
950951
try await testManifestWritingRoundTrip(manifestContents: contents, toolsVersion: .v6_2)
951952
}
953+
954+
func testTraits() async throws {
955+
try XCTSkipIfCompilerLessThan6_1()
956+
957+
let manifestContents = """
958+
// swift-tools-version: 6.1
959+
import PackageDescription
960+
961+
let package = Package(
962+
name: "TraitExample",
963+
traits: [
964+
"Foo",
965+
.trait(
966+
name: "Bar",
967+
enabledTraits: [
968+
"Foo",
969+
]
970+
),
971+
.trait(
972+
name: "FooBar",
973+
enabledTraits: [
974+
"Foo",
975+
"Bar",
976+
]
977+
),
978+
.default(enabledTraits: ["Foo"]),
979+
],
980+
dependencies: [
981+
.package(
982+
url: "https://github.com/Org/SomePackage.git",
983+
from: "1.0.0",
984+
traits: [
985+
.defaults,
986+
"SomeTrait",
987+
.trait(name: "SomeOtherTrait", condition: .when(traits: ["Foo"])),
988+
]
989+
),
990+
],
991+
targets: [
992+
.target(
993+
name: "SomeTarget",
994+
dependencies: [
995+
.product(
996+
name: "SomeProduct",
997+
package: "SomePackage",
998+
condition: .when(traits: ["Foo"])
999+
),
1000+
]
1001+
)
1002+
]
1003+
)
1004+
"""
1005+
try await testManifestWritingRoundTrip(manifestContents: manifestContents, toolsVersion: .v6_1)
1006+
}
9521007
}

0 commit comments

Comments
 (0)