-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add support for traits in manifest code generation #9173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,6 +102,11 @@ fileprivate extension SourceCodeFragment { | |
params.append(SourceCodeFragment(key: "products", subnodes: nodes)) | ||
} | ||
|
||
if !manifest.traits.isEmpty { | ||
let nodes = manifest.traits.map { SourceCodeFragment(from: $0) } | ||
params.append(SourceCodeFragment(key: "traits", subnodes: nodes)) | ||
} | ||
|
||
if !manifest.dependencies.isEmpty { | ||
let nodes = manifest.dependencies.map{ SourceCodeFragment(from: $0, pathAnchor: packageDirectory) } | ||
params.append(SourceCodeFragment(key: "dependencies", subnodes: nodes)) | ||
|
@@ -192,9 +197,68 @@ fileprivate extension SourceCodeFragment { | |
params.append(SourceCodeFragment("\"\(range.lowerBound)\"..<\"\(range.upperBound)\"")) | ||
} | ||
} | ||
|
||
if let traits = dependency.traits { | ||
// If only `.defaults` is specified, do not output `traits:` . | ||
// This is because `traits:` is not available in toolchains earlier than 6.1. | ||
let isDefault = traits.count == 1 && | ||
traits.allSatisfy(\.isDefaultsCase) | ||
|
||
if !isDefault { | ||
let traits = traits.sorted { a, b in | ||
PackageDependency.Trait.precedes(a, b) | ||
} | ||
params.append( | ||
SourceCodeFragment( | ||
key: "traits", | ||
subnodes: traits.map { SourceCodeFragment(from: $0) } | ||
) | ||
) | ||
} | ||
} | ||
|
||
self.init(enum: "package", subnodes: params) | ||
} | ||
|
||
|
||
init(from trait: PackageDependency.Trait) { | ||
if trait.isDefaultsCase { | ||
self.init(enum: "defaults") | ||
return | ||
} | ||
|
||
guard let condition = trait.condition else { | ||
self.init(string: trait.name) | ||
return | ||
} | ||
|
||
let conditionNode = SourceCodeFragment( | ||
key: "condition", | ||
subnode: SourceCodeFragment(from: condition) | ||
) | ||
|
||
self.init(enum: "trait", subnodes: [ | ||
SourceCodeFragment(key: "name", string: trait.name), | ||
conditionNode | ||
]) | ||
} | ||
|
||
init(from condition: PackageDependency.Trait.Condition) { | ||
var params: [SourceCodeFragment] = [] | ||
|
||
if let trait = condition.traits { | ||
params.append( | ||
SourceCodeFragment( | ||
key: "traits", | ||
subnodes: trait.sorted().map { | ||
SourceCodeFragment(string: $0) | ||
} | ||
) | ||
) | ||
} | ||
|
||
self.init(enum: "when", subnodes: params) | ||
} | ||
|
||
/// Instantiates a SourceCodeFragment to represent a single product. If there's a custom product generator, it gets | ||
/// a chance to generate the source code fragments before checking the default types. | ||
init(from product: ProductDescription, customProductTypeSourceGenerator: ManifestCustomProductTypeSourceGenerator?, toolsVersion: ToolsVersion) rethrows { | ||
|
@@ -261,6 +325,41 @@ fileprivate extension SourceCodeFragment { | |
} | ||
} | ||
|
||
init(from trait: TraitDescription) { | ||
let enabledTraitsNode = SourceCodeFragment( | ||
key: "enabledTraits", | ||
subnodes: trait.enabledTraits.sorted().map { | ||
SourceCodeFragment(string: $0) | ||
} | ||
) | ||
|
||
if trait.isDefault { | ||
self.init(enum: "default", subnodes: [enabledTraitsNode]) | ||
return | ||
} | ||
|
||
if trait.description == nil, trait.enabledTraits.isEmpty { | ||
self.init(string: trait.name) | ||
return | ||
} | ||
|
||
var params: [SourceCodeFragment] = [ | ||
SourceCodeFragment(key: "name", string: trait.name) | ||
] | ||
|
||
if let description = trait.description { | ||
params.append( | ||
SourceCodeFragment(key: "description", string: description) | ||
) | ||
} | ||
|
||
if !trait.enabledTraits.isEmpty { | ||
params.append(enabledTraitsNode) | ||
} | ||
|
||
self.init(enum: "trait", subnodes: params) | ||
} | ||
|
||
/// Instantiates a SourceCodeFragment to represent a single target. | ||
init(from target: TargetDescription) { | ||
var params: [SourceCodeFragment] = [] | ||
|
@@ -416,6 +515,13 @@ fileprivate extension SourceCodeFragment { | |
if let configName = condition.config { | ||
params.append(SourceCodeFragment(key: "configuration", enum: configName)) | ||
} | ||
if let traits = condition.traits { | ||
params.append( | ||
SourceCodeFragment(key: "traits", subnodes: traits.sorted().map { trait in | ||
SourceCodeFragment(string: trait) | ||
}) | ||
) | ||
} | ||
self.init(enum: "when", subnodes: params) | ||
} | ||
|
||
|
@@ -1023,6 +1129,47 @@ public struct SourceCodeFragment { | |
} | ||
} | ||
|
||
extension Optional { | ||
fileprivate static func precedes( | ||
_ a: Wrapped?, _ b: Wrapped?, | ||
compareWrapped: (Wrapped, Wrapped) -> Bool | ||
) -> Bool { | ||
switch (a, b) { | ||
case (.none, .none): return false | ||
case (.none, .some): return true | ||
case (.some, .none): return false | ||
case (.some(let a), .some(let b)): | ||
return compareWrapped(a, b) | ||
} | ||
} | ||
} | ||
|
||
extension PackageDependency.Trait { | ||
fileprivate static func precedes(_ a: PackageDependency.Trait, _ b: PackageDependency.Trait) -> Bool { | ||
if a.name != b.name { return a.name < b.name } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: I'm not sure we can ever have two traits of the same name in a single package (cc @FranzBusch is this a correct assumption?), so sorting traits that originate from the same package can probably just be done by using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, if we unify the directives, sorting becomes simpler, which is nice, but... At present, the following is valid: dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.1", traits: [
.trait(name: "Foo", condition: .when(traits: ["Blue"])),
.trait(name: "Foo", condition: .when(traits: ["Green"])),
]),
], It can be unified as follows: dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.1", traits: [
.trait(name: "Foo", condition: .when(traits: ["Blue", "Green"])),
]),
], However, for example, if it is extended like this... dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.1", traits: [
.trait(name: "Foo", condition: .when(platforms: [.macOS], traits: ["Blue"])),
.trait(name: "Foo", condition: .when(platforms: [.linux], traits: ["Green"])),
]),
], This does not seem to be unifiable. In the future, if it ever comes to this, we might overlook the necessary adjustments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two parts to traits.
In the former, trait names can only be expressed once. In the latter, @omochi is right that it is totally valid to define an enabled trait multiple times with different conditions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @omochi Thank you for the detailed examples! This helps a lot. |
||
|
||
if a.condition != b.condition { | ||
return Optional.precedes(a.condition, b.condition) { a, b in | ||
PackageDependency.Trait.Condition.precedes(a, b) | ||
} | ||
} | ||
|
||
return false | ||
} | ||
} | ||
|
||
extension PackageDependency.Trait.Condition { | ||
fileprivate static func precedes(_ a: PackageDependency.Trait.Condition, _ b: PackageDependency.Trait.Condition) -> Bool { | ||
if a.traits != b.traits { | ||
return Optional.precedes(a.traits, b.traits) { a, b in | ||
a.sorted().lexicographicallyPrecedes(b.sorted()) | ||
} | ||
} | ||
|
||
return false | ||
} | ||
} | ||
|
||
extension TargetBuildSettingDescription.Kind { | ||
fileprivate var name: String { | ||
switch self { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be public?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the lack of explanation.
The main purpose of this patch is not to make it
public
.However, since
PackageModel.TraitDescription.isDefault
already exists and ispublic
, I thought it would be natural to makePackageModel.PackageDependency.Trait.isDefaultsCase
public as well.In particular, for this part, the user-facing API uses
.defaults
, while the internal representation uses"default"
, which do not match. This makes it easy to make mistakes during implementation, so I thought it would be helpful to provide an API.