-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Fix Command Resolution #4038
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
The head ref may contain hidden characters: "commands\u2010pr"
Fix Command Resolution #4038
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 |
---|---|---|
|
@@ -58,23 +58,38 @@ public enum DependencyResolutionNode { | |
/// to the information needed in order to provide actionable suggestions to help the user stitch up the dependency declarations properly. | ||
case root(package: PackageReference) | ||
|
||
// FIXME: If command plugins become explicit in the manifest, or are extricated from the main graph, `implicitCommands` should be removed. | ||
/// A node representing any implicit command plugins vended by a package. | ||
/// | ||
/// This type of node is equivalent to a product node in all respects, | ||
/// except that the number and names of the products are unkown, | ||
/// because the manifest contains no explicit reference to them. | ||
case implicitCommands(package: PackageReference) | ||
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. It is too bad we need a whole new kind of node, but since we cannot know the product names before the dependency is fetched, we have no other choice. |
||
|
||
/// The package. | ||
public var package: PackageReference { | ||
switch self { | ||
case .empty(let package), .product(_, let package), .root(let package): | ||
case .empty(let package), .product(_, let package), .root(let package), .implicitCommands(package: let package): | ||
return package | ||
} | ||
} | ||
|
||
/// The name of the specific product if the node is a product node, otherwise `nil`. | ||
public var specificProduct: String? { | ||
switch self { | ||
case .empty, .root: | ||
case .empty, .root, .implicitCommands: | ||
return nil | ||
case .product(let product, _): | ||
return product | ||
} | ||
} | ||
public var isImplicitCommandsNode: Bool { | ||
if case .implicitCommands = self { | ||
return true | ||
} else { | ||
return false | ||
} | ||
} | ||
|
||
/// Assembles the product filter to use on the manifest for this node to determine its dependencies. | ||
public var productFilter: ProductFilter { | ||
|
@@ -85,6 +100,8 @@ public enum DependencyResolutionNode { | |
return .specific([product]) | ||
case .root: | ||
return .everything | ||
case .implicitCommands: | ||
return .specific([], includeCommandPlugins: true) | ||
} | ||
} | ||
|
||
|
@@ -93,7 +110,7 @@ public enum DependencyResolutionNode { | |
/// This is the constraint that requires all products from a package resolve to the same version. | ||
internal func versionLock(version: Version) -> PackageContainerConstraint? { | ||
// Don’t create a version lock for anything but a product. | ||
guard specificProduct != nil else { return nil } | ||
guard specificProduct != nil || isImplicitCommandsNode else { return nil } | ||
return PackageContainerConstraint( | ||
package: self.package, | ||
versionRequirement: .exact(version), | ||
|
@@ -106,7 +123,7 @@ public enum DependencyResolutionNode { | |
/// This is the constraint that requires all products from a package resolve to the same revision. | ||
internal func revisionLock(revision: String) -> PackageContainerConstraint? { | ||
// Don’t create a revision lock for anything but a product. | ||
guard specificProduct != nil else { return nil } | ||
guard specificProduct != nil || isImplicitCommandsNode else { return nil } | ||
return PackageContainerConstraint( | ||
package: self.package, | ||
requirement: .revision(revision), | ||
|
@@ -117,14 +134,17 @@ public enum DependencyResolutionNode { | |
|
||
extension DependencyResolutionNode: Equatable { | ||
public static func ==(lhs: DependencyResolutionNode, rhs: DependencyResolutionNode) -> Bool { | ||
return (lhs.package, lhs.specificProduct) == (rhs.package, rhs.specificProduct) | ||
return lhs.package == rhs.package | ||
&& lhs.specificProduct == rhs.specificProduct | ||
&& lhs.isImplicitCommandsNode == rhs.isImplicitCommandsNode | ||
} | ||
} | ||
|
||
extension DependencyResolutionNode: Hashable { | ||
public func hash(into hasher: inout Hasher) { | ||
hasher.combine(self.package) | ||
hasher.combine(self.specificProduct) | ||
hasher.combine(self.isImplicitCommandsNode) | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,12 +55,17 @@ extension PackageContainerConstraint { | |
case .everything: | ||
assertionFailure("Attempted to enumerate a root package’s product filter; root packages have no filter.") | ||
return [] | ||
case .specific(let set): | ||
case .specific(let set, let includeCommandPlugins): | ||
var nodes: [DependencyResolutionNode] | ||
if set.isEmpty { // Pointing at the package without a particular product. | ||
return [.empty(package: self.package)] | ||
nodes = [.empty(package: self.package)] | ||
} else { | ||
return set.sorted().map { .product($0, package: self.package) } | ||
nodes = set.sorted().map { .product($0, package: self.package) } | ||
} | ||
if includeCommandPlugins { | ||
nodes.append(.implicitCommands(package: self.package)) | ||
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. This is where the new resolution node is first inserted. |
||
} | ||
return nodes | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -150,6 +150,24 @@ public final class Manifest { | |
self.targetMap = Dictionary(targets.lazy.map({ ($0.name, $0) }), uniquingKeysWith: { $1 }) | ||
} | ||
|
||
public func productIsCommandPlugin(_ product: ProductDescription) -> Bool { | ||
return product.type == .plugin | ||
&& self.targets.contains(where: { target in | ||
if case .command = target.pluginCapability, | ||
product.targets.contains(target.name) { | ||
return true | ||
} else { | ||
return false | ||
} | ||
}) | ||
} | ||
internal func productIsCommandPlugin(_ product: String) -> Bool { | ||
return products.contains(where: { possibleMatch in | ||
return possibleMatch.name == product | ||
&& self.productIsCommandPlugin(possibleMatch) | ||
}) | ||
} | ||
|
||
/// Returns the targets required for a particular product filter. | ||
public func targetsRequired(for productFilter: ProductFilter) -> [TargetDescription] { | ||
#if ENABLE_TARGET_BASED_DEPENDENCY_RESOLUTION | ||
|
@@ -161,8 +179,11 @@ public final class Manifest { | |
switch productFilter { | ||
case .everything: | ||
return self.targets | ||
case .specific(let productFilter): | ||
let products = self.products.filter { productFilter.contains($0.name) } | ||
case .specific(let productFilter, let includeCommandPlugins): | ||
let products = self.products.filter { product in | ||
return productFilter.contains(product.name) | ||
|| (includeCommandPlugins && productIsCommandPlugin(product)) | ||
} | ||
targets = targetsRequired(for: products) | ||
} | ||
|
||
|
@@ -182,7 +203,11 @@ public final class Manifest { | |
return dependencies | ||
} else { | ||
let targets = self.targetsRequired(for: productFilter) | ||
let dependencies = self.dependenciesRequired(for: targets, keepUnused: productFilter == .everything) | ||
let dependencies = self.dependenciesRequired( | ||
for: targets, | ||
keepImplicitCommandPlugins: productFilter == .everything, | ||
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. The manifest is a root if |
||
keepUnused: productFilter == .everything | ||
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. This is what triggers the partial loading of direct dependency manifests that I mentioned on the other thread. That is why it has the same condition. I considered unifying them, since they are always the same during normal execution, but decided against it because the function does not specify that semantic and because tests exercise it in other ways. |
||
) | ||
self._requiredDependencies[productFilter] = dependencies | ||
return dependencies | ||
} | ||
|
@@ -249,6 +274,7 @@ public final class Manifest { | |
/// The returned dependencies have their particular product filters registered. (To determine product filters without removing any dependencies from the list, specify `keepUnused: true`.) | ||
private func dependenciesRequired( | ||
for targets: [TargetDescription], | ||
keepImplicitCommandPlugins: Bool, | ||
keepUnused: Bool = false | ||
) -> [PackageDependency] { | ||
|
||
|
@@ -276,13 +302,13 @@ public final class Manifest { | |
|
||
return dependencies.compactMap { dependency in | ||
if let filter = associations[dependency.identity] { | ||
return dependency.filtered(by: filter) | ||
return dependency.filtered(by: keepImplicitCommandPlugins ? filter.includingImplicitCommands() : filter) | ||
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. This (and the following three lines) are where the manifest realizes these are direct dependencies and decides that any command plugins still matter. |
||
} else if keepUnused { | ||
// Register that while the dependency was kept, no products are needed. | ||
return dependency.filtered(by: .nothing) | ||
return dependency.filtered(by: keepImplicitCommandPlugins ? .nothing.includingImplicitCommands() : .nothing) | ||
} else { | ||
// Dependencies known to not have any relevant products are discarded. | ||
return nil | ||
return keepImplicitCommandPlugins ? dependency.filtered(by: .nothing.includingImplicitCommands()) : nil | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,15 +107,18 @@ public enum ProductType: Equatable, Hashable { | |
/// Any product which matches the filter will be used for dependency resolution, whereas unrequested products will be ignored. | ||
/// | ||
/// Requested products need not actually exist in the package. Under certain circumstances, the resolver may request names whose package of origin are unknown. The intended package will recognize and fulfill the request; packages that do not know what it is will simply ignore it. | ||
public enum ProductFilter: Equatable, Hashable { | ||
public enum ProductFilter: Codable, Equatable, Hashable { | ||
|
||
/// All products, targets, and tests are requested. | ||
/// | ||
/// This is used for root packages. | ||
case everything | ||
|
||
// FIXME: If command plugins become explicit in the manifest, or are extricated from the main graph, `includeCommandPlugins` should be removed. | ||
/// A set of specific products requested by one or more client packages. | ||
case specific(Set<String>) | ||
/// | ||
/// `includeCommandPlugins` is used by first‐level dependencies to also request any command plugins, regardless of whether they are referenced anywhere. | ||
case specific(Set<String>, includeCommandPlugins: Bool = false) | ||
|
||
/// No products, targets, or tests are requested. | ||
public static var nothing: ProductFilter { .specific([]) } | ||
|
@@ -124,12 +127,15 @@ public enum ProductFilter: Equatable, Hashable { | |
switch self { | ||
case .everything: | ||
return .everything | ||
case .specific(let set): | ||
case .specific(let set, let includeCommandPlugins): | ||
switch other { | ||
case .everything: | ||
return .everything | ||
case .specific(let otherSet): | ||
return .specific(set.union(otherSet)) | ||
case .specific(let otherSet, let otherIncludeCommandPlugins): | ||
return .specific( | ||
set.union(otherSet), | ||
includeCommandPlugins: includeCommandPlugins || otherIncludeCommandPlugins | ||
) | ||
} | ||
} | ||
} | ||
|
@@ -138,23 +144,22 @@ public enum ProductFilter: Equatable, Hashable { | |
self = self.union(other) | ||
} | ||
|
||
public func contains(_ product: String) -> Bool { | ||
public func contains(_ product: String, isCommandPlugin: (String) -> Bool) -> Bool { | ||
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. This function now needs outside information. The only call sites are tests, so I considered removing it and refactoring the tests. But since it is public, I don’t know what other clients might be using it. So I kept it around with the smallest change possible to still be correct. 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. That makes sense. Because SwiftPM doesn't yet have a versioned API I suspect that any clients could adapt, but passing in the closure makes sense too. |
||
switch self { | ||
case .everything: | ||
return true | ||
case .specific(let set): | ||
case .specific(let set, let includeCommandPlugins): | ||
return set.contains(product) | ||
|| (includeCommandPlugins && isCommandPlugin(product)) | ||
} | ||
} | ||
|
||
public func merge(_ other: ProductFilter) -> ProductFilter { | ||
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. This function was redundant. |
||
switch (self, other) { | ||
case (.everything, _): | ||
return .everything | ||
case (_, .everything): | ||
return .everything | ||
case (.specific(let mine), .specific(let other)): | ||
return .specific(mine.union(other)) | ||
internal func includingImplicitCommands() -> ProductFilter { | ||
switch self { | ||
case .everything: | ||
return self | ||
case .specific(let set, _): | ||
return .specific(set, includeCommandPlugins: true) | ||
} | ||
} | ||
} | ||
|
@@ -197,8 +202,8 @@ extension ProductFilter: CustomStringConvertible { | |
switch self { | ||
case .everything: | ||
return "[everything]" | ||
case .specific(let set): | ||
return "[\(set.sorted().joined(separator: ", "))]" | ||
case .specific(let set, let includeCommandPlugins): | ||
return "[\(set.sorted().joined(separator: ", "))\(includeCommandPlugins ? " (including command plugins)" : "")]" | ||
} | ||
} | ||
} | ||
|
@@ -248,26 +253,3 @@ extension ProductType: Codable { | |
} | ||
} | ||
} | ||
|
||
extension ProductFilter: Codable { | ||
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. The old I do not know where or if this is actually used. We stopped recording filters in the pins file a while ago. Changing it did not flag on any tests. Do you think there is anywhere we need to bump a format version? 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. |
||
public func encode(to encoder: Encoder) throws { | ||
let optionalSet: Set<String>? | ||
switch self { | ||
case .everything: | ||
optionalSet = nil | ||
case .specific(let set): | ||
optionalSet = set | ||
} | ||
var container = encoder.singleValueContainer() | ||
try container.encode(optionalSet?.sorted()) | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.singleValueContainer() | ||
if container.decodeNil() { | ||
self = .everything | ||
} else { | ||
self = .specific(Set(try container.decode([String].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.
👍