Skip to content

Commit 75936ef

Browse files
Finish SE‐0226 (Ignore Unused Products) (#2749)
* Finished SE‐0226. * Fixed documentation typo. * Set default XCTest deployment targets. * Adjusted intents to 4 spaces. * Converted DependencyResolutionNode to an enumeration. * Simplified node iteration. * Abbreviated binding with typealias. * Broke up fix‐me.
1 parent 809eea6 commit 75936ef

34 files changed

+1862
-872
lines changed

Fixtures/ModuleMaps/Transitive/packageC/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ let package = Package(
1010
.package(url: "../packageD", from: "1.0.0"),
1111
],
1212
targets: [
13-
.target(name: "x", dependencies: []),
13+
.target(name: "x", dependencies: ["CFoo"]),
1414
]
1515
)

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ let package = Package(
224224
.testTarget(
225225
name: "XCBuildSupportTests",
226226
dependencies: ["XCBuildSupport", "SPMTestSupport"]),
227+
228+
// Examples (These are built to ensure they stay up to date with the API.)
229+
.target(
230+
name: "package-info",
231+
dependencies: ["PackageModel", "PackageLoading", "PackageGraph", "Workspace"],
232+
path: "Examples/package-info/Sources/package-info"
233+
)
227234
],
228235
swiftLanguageVersions: [.v5]
229236
)

Sources/Commands/SwiftBuildTool.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class SwiftBuildTool: SwiftTool<BuildToolOptions> {
3737
#endif
3838

3939
guard let subset = options.buildSubset(diagnostics: diagnostics) else { return }
40-
let buildSystem = try createBuildSystem()
40+
let buildSystem = try createBuildSystem(explicitProduct: options.product)
4141
try buildSystem.build(subset: subset)
4242

4343
case .binPath:

Sources/Commands/SwiftPackageTool.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public class SwiftPackageTool: SwiftTool<PackageToolOptions> {
114114

115115
let builder = PackageBuilder(
116116
manifest: manifest,
117+
productFilter: .everything,
117118
path: try getPackageRoot(),
118119
xcTestMinimumDeploymentTargets: [:], // Minimum deployment target does not matter for this operation.
119120
diagnostics: diagnostics
@@ -354,6 +355,7 @@ public class SwiftPackageTool: SwiftTool<PackageToolOptions> {
354355

355356
let builder = PackageBuilder(
356357
manifest: manifest,
358+
productFilter: .everything,
357359
path: try getPackageRoot(),
358360
xcTestMinimumDeploymentTargets: MinimumDeploymentTarget.default.xcTestMinimumDeploymentTargets,
359361
diagnostics: diagnostics
@@ -933,10 +935,10 @@ fileprivate extension SwiftPackageTool {
933935
for (package, change) in changes {
934936
let currentVersion = pins.pinsMap[package.identity]?.state.description ?? ""
935937
switch change {
936-
case let .added(requirement):
937-
stream <<< "+ \(package.name) \(requirement.prettyPrinted)"
938-
case let .updated(requirement):
939-
stream <<< "~ \(package.name) \(currentVersion) -> \(package.name) \(requirement.prettyPrinted)"
938+
case let .added(state):
939+
stream <<< "+ \(package.name) \(state.requirement.prettyPrinted)"
940+
case let .updated(state):
941+
stream <<< "~ \(package.name) \(currentVersion) -> \(package.name) \(state.requirement.prettyPrinted)"
940942
case .removed:
941943
stream <<< "- \(package.name) \(currentVersion)"
942944
case .unchanged:

Sources/Commands/SwiftRunTool.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@ public class SwiftRunTool: SwiftTool<RunToolOptions> {
9595

9696
case .repl:
9797
// Load a custom package graph which has a special product for REPL.
98-
let graphLoader = { try self.loadPackageGraph(createREPLProduct: self.options.shouldLaunchREPL) }
98+
let graphLoader = {
99+
try self.loadPackageGraph(
100+
explicitProduct: self.options.executable,
101+
createREPLProduct: self.options.shouldLaunchREPL)
102+
}
99103
let buildParameters = try self.buildParameters()
100104

101105
// Construct the build operation.
@@ -140,7 +144,7 @@ public class SwiftRunTool: SwiftTool<RunToolOptions> {
140144
return
141145
}
142146

143-
let buildSystem = try createBuildSystem()
147+
let buildSystem = try createBuildSystem(explicitProduct: options.executable)
144148
let productName = try findProductName(in: buildSystem.getPackageGraph())
145149

146150
if options.shouldBuildTests {

Sources/Commands/SwiftTool.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -686,8 +686,12 @@ public class SwiftTool<Options: ToolOptions> {
686686
}
687687

688688
/// Fetch and load the complete package graph.
689+
///
690+
/// - Parameters:
691+
/// - explicitProduct: The product specified on the command line to a “swift run” or “swift build” command. This allows executables from dependencies to be run directly without having to hook them up to any particular target.
689692
@discardableResult
690693
func loadPackageGraph(
694+
explicitProduct: String? = nil,
691695
createMultipleTestProducts: Bool = false,
692696
createREPLProduct: Bool = false
693697
) throws -> PackageGraph {
@@ -697,6 +701,7 @@ public class SwiftTool<Options: ToolOptions> {
697701
// Fetch and load the package graph.
698702
let graph = try workspace.loadPackageGraph(
699703
root: getWorkspaceRoot(),
704+
explicitProduct: explicitProduct,
700705
createMultipleTestProducts: createMultipleTestProducts,
701706
createREPLProduct: createREPLProduct,
702707
forceResolvedVersions: options.forceResolvedVersions,
@@ -739,9 +744,9 @@ public class SwiftTool<Options: ToolOptions> {
739744
return enableBuildManifestCaching && haveBuildManifestAndDescription && !hasEditedPackages
740745
}
741746

742-
func createBuildOperation(useBuildManifestCaching: Bool = true) throws -> BuildOperation {
747+
func createBuildOperation(explicitProduct: String? = nil, useBuildManifestCaching: Bool = true) throws -> BuildOperation {
743748
// Load a custom package graph which has a special product for REPL.
744-
let graphLoader = { try self.loadPackageGraph() }
749+
let graphLoader = { try self.loadPackageGraph(explicitProduct: explicitProduct) }
745750

746751
// Construct the build operation.
747752
let buildOp = try BuildOperation(
@@ -757,11 +762,11 @@ public class SwiftTool<Options: ToolOptions> {
757762
return buildOp
758763
}
759764

760-
func createBuildSystem(useBuildManifestCaching: Bool = true) throws -> BuildSystem {
765+
func createBuildSystem(explicitProduct: String? = nil, useBuildManifestCaching: Bool = true) throws -> BuildSystem {
761766
let buildSystem: BuildSystem
762767
switch options.buildSystem {
763768
case .native:
764-
let graphLoader = { try self.loadPackageGraph() }
769+
let graphLoader = { try self.loadPackageGraph(explicitProduct: explicitProduct) }
765770
buildSystem = try BuildOperation(
766771
buildParameters: buildParameters(),
767772
useBuildManifestCaching: useBuildManifestCaching && canUseBuildManifestCaching(),
@@ -770,7 +775,7 @@ public class SwiftTool<Options: ToolOptions> {
770775
stdoutStream: stdoutStream
771776
)
772777
case .xcode:
773-
let graphLoader = { try self.loadPackageGraph(createMultipleTestProducts: true) }
778+
let graphLoader = { try self.loadPackageGraph(explicitProduct: explicitProduct, createMultipleTestProducts: true) }
774779
buildSystem = try XcodeBuildSystem(
775780
buildParameters: buildParameters(),
776781
packageGraphLoader: graphLoader,

Sources/PackageGraph/DependencyResolver.swift

Lines changed: 144 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
import TSCBasic
12-
import struct PackageModel.PackageReference
12+
import PackageModel
1313
import struct TSCUtility.Version
1414
import class Foundation.NSDate
1515

@@ -111,7 +111,7 @@ public protocol PackageContainer {
111111
/// - Precondition: `versions.contains(version)`
112112
/// - Throws: If the version could not be resolved; this will abort
113113
/// dependency resolution completely.
114-
func getDependencies(at version: Version) throws -> [PackageContainerConstraint]
114+
func getDependencies(at version: Version, productFilter: ProductFilter) throws -> [PackageContainerConstraint]
115115

116116
/// Fetch the declared dependencies for a particular revision.
117117
///
@@ -120,12 +120,12 @@ public protocol PackageContainer {
120120
///
121121
/// - Throws: If the revision could not be resolved; this will abort
122122
/// dependency resolution completely.
123-
func getDependencies(at revision: String) throws -> [PackageContainerConstraint]
123+
func getDependencies(at revision: String, productFilter: ProductFilter) throws -> [PackageContainerConstraint]
124124

125125
/// Fetch the dependencies of an unversioned package container.
126126
///
127127
/// NOTE: This method should not be called on a versioned container.
128-
func getUnversionedDependencies() throws -> [PackageContainerConstraint]
128+
func getUnversionedDependencies(productFilter: ProductFilter) throws -> [PackageContainerConstraint]
129129

130130
/// Get the updated identifier at a bound version.
131131
///
@@ -154,21 +154,25 @@ public struct PackageContainerConstraint: CustomStringConvertible, Equatable, Ha
154154
/// The constraint requirement.
155155
public let requirement: PackageRequirement
156156

157+
/// The required products.
158+
public let products: ProductFilter
159+
157160
/// Create a constraint requiring the given `container` satisfying the
158161
/// `requirement`.
159-
public init(container identifier: PackageReference, requirement: PackageRequirement) {
162+
public init(container identifier: PackageReference, requirement: PackageRequirement, products: ProductFilter) {
160163
self.identifier = identifier
161164
self.requirement = requirement
165+
self.products = products
162166
}
163167

164168
/// Create a constraint requiring the given `container` satisfying the
165169
/// `versionRequirement`.
166-
public init(container identifier: PackageReference, versionRequirement: VersionSetSpecifier) {
167-
self.init(container: identifier, requirement: .versionSet(versionRequirement))
170+
public init(container identifier: PackageReference, versionRequirement: VersionSetSpecifier, products: ProductFilter) {
171+
self.init(container: identifier, requirement: .versionSet(versionRequirement), products: products)
168172
}
169173

170174
public var description: String {
171-
return "Constraint(\(identifier), \(requirement))"
175+
return "Constraint(\(identifier), \(requirement), \(products)"
172176
}
173177
}
174178

@@ -209,7 +213,7 @@ public enum BoundVersion: Equatable, CustomStringConvertible {
209213
}
210214

211215
public class DependencyResolver {
212-
public typealias Binding = (container: PackageReference, binding: BoundVersion)
216+
public typealias Binding = (container: PackageReference, binding: BoundVersion, products: ProductFilter)
213217

214218
/// The dependency resolver result.
215219
public enum Result {
@@ -220,3 +224,134 @@ public class DependencyResolver {
220224
case error(Swift.Error)
221225
}
222226
}
227+
228+
/// A node in the dependency resolution graph.
229+
///
230+
/// See the documentation of each case for more detailed descriptions of each kind and how they interact.
231+
///
232+
/// - SeeAlso: `GraphLoadingNode`
233+
public enum DependencyResolutionNode: Equatable, Hashable, CustomStringConvertible {
234+
235+
/// An empty package node.
236+
///
237+
/// This node indicates that a package needs to be present, but does not indicate that any of its contents are needed.
238+
///
239+
/// Empty package nodes are always leaf nodes; they have no dependencies.
240+
case empty(package: PackageReference)
241+
242+
/// A product node.
243+
///
244+
/// This node indicates that a particular product in a particular package is required.
245+
///
246+
/// Product nodes always have dependencies. A product node has...
247+
///
248+
/// - one implicit dependency on its own package at an exact version (as an empty package node).
249+
/// This dependency is what ensures the resolver does not select two products from the same package at different versions.
250+
/// - zero or more dependencies on the product nodes of other packages.
251+
/// These are all the external products required to build all of the targets vended by this product.
252+
/// They derive from the manifest.
253+
///
254+
/// Tools versions before 5.2 do not know which products belong to which packages, so each product is required from every dependency.
255+
/// Since a non‐existant product ends up with only its implicit dependency on its own package,
256+
/// only whichever package contains the product will end up adding additional constraints.
257+
/// See `ProductFilter` and `Manifest.register(...)`.
258+
case product(String, package: PackageReference)
259+
260+
/// A root node.
261+
///
262+
/// This node indicates a root node in the graph, which is required no matter what.
263+
///
264+
/// Root nodes may have dependencies. A root node has...
265+
///
266+
/// - zero or more dependencies on each external product node required to build any of its targets (vended or not).
267+
/// - zero or more dependencies directly on external empty package nodes.
268+
/// This special case occurs when a dependecy is declared but not used.
269+
/// It is a warning condition, and builds do not actually need these dependencies.
270+
/// However, forcing the graph to resolve and fetch them anyway allows the diagnostics passes access
271+
/// to the information needed in order to provide actionable suggestions to help the user stitch up the dependency declarations properly.
272+
case root(package: PackageReference)
273+
274+
/// The package.
275+
public var package: PackageReference {
276+
switch self {
277+
case .empty(let package), .product(_, let package), .root(let package):
278+
return package
279+
}
280+
}
281+
282+
/// The name of the specific product if the node is a product node, otherwise `nil`.
283+
public var specificProduct: String? {
284+
switch self {
285+
case .empty, .root:
286+
return nil
287+
case .product(let product, _):
288+
return product
289+
}
290+
}
291+
292+
// To ensure cyclical dependencies are detected properly,
293+
// hashing cannot include whether the node behaves as a root.
294+
private struct Identity: Equatable, Hashable {
295+
fileprivate let package: PackageReference
296+
fileprivate let specificProduct: String?
297+
}
298+
private var identity: Identity {
299+
return Identity(package: package, specificProduct: specificProduct)
300+
}
301+
public static func ==(lhs: DependencyResolutionNode, rhs: DependencyResolutionNode) -> Bool {
302+
return lhs.identity == rhs.identity
303+
}
304+
public func hash(into hasher: inout Hasher) {
305+
hasher.combine(identity)
306+
}
307+
308+
/// Assembles the product filter to use on the manifest for this node to determine it’s dependencies.
309+
internal func productFilter() -> ProductFilter {
310+
switch self {
311+
case .empty:
312+
return .specific([])
313+
case .product(let product, _):
314+
return .specific([product])
315+
case .root:
316+
return .everything
317+
}
318+
}
319+
320+
/// Returns the dependency that a product has on its own package, if relevant.
321+
///
322+
/// This is the constraint that requires all products from a package resolve to the same version.
323+
internal func versionLock(version: Version) -> RepositoryPackageConstraint? {
324+
// Don’t create a version lock for anything but a product.
325+
guard specificProduct != nil else { return nil }
326+
return RepositoryPackageConstraint(
327+
container: package,
328+
versionRequirement: .exact(version),
329+
products: .specific([])
330+
)
331+
}
332+
333+
/// Returns the dependency that a product has on its own package, if relevant.
334+
///
335+
/// This is the constraint that requires all products from a package resolve to the same revision.
336+
internal func revisionLock(revision: String) -> RepositoryPackageConstraint? {
337+
// Don’t create a revision lock for anything but a product.
338+
guard specificProduct != nil else { return nil }
339+
return RepositoryPackageConstraint(
340+
container: package,
341+
requirement: .revision(revision),
342+
products: .specific([])
343+
)
344+
}
345+
346+
public var description: String {
347+
return "\(package.name)\(productFilter())"
348+
}
349+
350+
public func nameForDiagnostics() -> String {
351+
if let product = specificProduct {
352+
return "\(package.name)[\(product)]"
353+
} else {
354+
return "\(package.name)"
355+
}
356+
}
357+
}

0 commit comments

Comments
 (0)