Skip to content

Commit 6eb042d

Browse files
committed
[PubGrub] Avoid resolving package versions twice in presence of prebuilt libraries
Integrate information about versions of prebuilt libraries into the solver and prioritize these versions when making decisions if they meet all of the requirements of undecided terms. This helps solver to avoid fetching and at the same time allows partual matches on prebuilt libraries with fallback to package provided versions.
1 parent 26b4ed0 commit 6eb042d

File tree

6 files changed

+98
-134
lines changed

6 files changed

+98
-134
lines changed

Sources/PackageGraph/Resolution/PubGrub/ContainerProvider.swift

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,13 @@ final class ContainerProvider {
6565
/// Get the container for the given identifier, loading it if necessary.
6666
func getContainer(
6767
for package: PackageReference,
68-
availableLibraries: [LibraryMetadata],
6968
completion: @escaping (Result<PubGrubPackageContainer, Error>) -> Void
7069
) {
7170
// Return the cached container, if available.
7271
if let container = self.containersCache[comparingLocation: package] {
7372
return completion(.success(container))
7473
}
7574

76-
if let metadata = package.matchingPrebuiltLibrary(in: availableLibraries) {
77-
do {
78-
let prebuiltPackageContainer = try PrebuiltPackageContainer(metadata: metadata)
79-
let pubGrubContainer = PubGrubPackageContainer(underlying: prebuiltPackageContainer, pins: self.pins)
80-
self.containersCache[package] = pubGrubContainer
81-
return completion(.success(pubGrubContainer))
82-
} catch {
83-
return completion(.failure(error))
84-
}
85-
}
86-
8775
if let prefetchSync = self.prefetches[package] {
8876
// If this container is already being prefetched, wait for that to complete
8977
prefetchSync.notify(queue: .sharedConcurrent) {
@@ -93,7 +81,7 @@ final class ContainerProvider {
9381
} else {
9482
// if prefetch failed, remove from list of prefetches and try again
9583
self.prefetches[package] = nil
96-
return self.getContainer(for: package, availableLibraries: availableLibraries, completion: completion)
84+
return self.getContainer(for: package, completion: completion)
9785
}
9886
}
9987
} else {

Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ public struct PubGrubDependencyResolver {
103103
/// Reference to the pins store, if provided.
104104
private let pins: PinsStore.Pins
105105

106+
/// The packages that are available in a prebuilt form in SDK or a toolchain
107+
private let availableLibraries: [LibraryMetadata]
108+
106109
/// The container provider used to load package containers.
107110
private let provider: ContainerProvider
108111

@@ -121,13 +124,15 @@ public struct PubGrubDependencyResolver {
121124
public init(
122125
provider: PackageContainerProvider,
123126
pins: PinsStore.Pins = [:],
127+
availableLibraries: [LibraryMetadata] = [],
124128
skipDependenciesUpdates: Bool = false,
125129
prefetchBasedOnResolvedFile: Bool = false,
126130
observabilityScope: ObservabilityScope,
127131
delegate: DependencyResolverDelegate? = nil
128132
) {
129133
self.packageContainerProvider = provider
130134
self.pins = pins
135+
self.availableLibraries = availableLibraries
131136
self.skipDependenciesUpdates = skipDependenciesUpdates
132137
self.prefetchBasedOnResolvedFile = prefetchBasedOnResolvedFile
133138
self.provider = ContainerProvider(
@@ -140,11 +145,7 @@ public struct PubGrubDependencyResolver {
140145
}
141146

142147
/// Execute the resolution algorithm to find a valid assignment of versions.
143-
public func solve(constraints: [Constraint], availableLibraries: [LibraryMetadata], preferPrebuiltLibraries: Bool) -> Result<[DependencyResolverBinding], Error> {
144-
if !preferPrebuiltLibraries {
145-
self.provider.removeCachedContainers(for: availableLibraries.flatMap { $0.identities.map { $0.ref } })
146-
}
147-
148+
public func solve(constraints: [Constraint]) -> Result<[DependencyResolverBinding], Error> {
148149
// the graph resolution root
149150
let root: DependencyResolutionNode
150151
if constraints.count == 1, let constraint = constraints.first, constraint.package.kind.isRoot {
@@ -159,26 +160,24 @@ public struct PubGrubDependencyResolver {
159160
}
160161

161162
do {
162-
// Use empty `availableLibraries` for the rest of resolving if we don't prefer them.
163-
let availableLibraries = preferPrebuiltLibraries ? availableLibraries : []
164-
// strips state
165-
let bindings = try self.solve(root: root, constraints: constraints, availableLibraries: availableLibraries).bindings.filter {
166-
return $0.package.matchingPrebuiltLibrary(in: availableLibraries) == nil
163+
// Strip packages that have prebuilt libraries only if they match library version.
164+
//
165+
// FIXME: This is built on assumption that libraries are part of the SDK and are
166+
// always available in include/library paths, but what happens if they are
167+
// part of a toolchain instead? Builder needs an indicator that certain path
168+
// has to be included when building packages that depend on prebuilt libraries.
169+
let bindings = try self.solve(root: root, constraints: constraints).bindings.filter {
170+
if case let .version(version) = $0.boundVersion {
171+
if let library = $0.package.matchingPrebuiltLibrary(in: self.availableLibraries) {
172+
return .init(stringLiteral: library.version) != version
173+
}
174+
}
175+
return true
167176
}
168177
return .success(bindings)
169178
} catch {
170179
// If version solving failing, build the user-facing diagnostic.
171180
if let pubGrubError = error as? PubgrubError, let rootCause = pubGrubError.rootCause, let incompatibilities = pubGrubError.incompatibilities {
172-
let incompatiblePackages = incompatibilities.map({ $0.key.package })
173-
if incompatiblePackages.contains(where: { $0.matchingPrebuiltLibrary(in: availableLibraries) != nil }) {
174-
return .failure(
175-
PubgrubError.potentiallyUnresovableDueToPrebuiltLibrary(
176-
incompatiblePackages,
177-
pubGrubError.description
178-
)
179-
)
180-
}
181-
182181
do {
183182
var builder = DiagnosticReportBuilder(
184183
root: root,
@@ -199,12 +198,12 @@ public struct PubGrubDependencyResolver {
199198
/// Find a set of dependencies that fit the given constraints. If dependency
200199
/// resolution is unable to provide a result, an error is thrown.
201200
/// - Warning: It is expected that the root package reference has been set before this is called.
202-
internal func solve(root: DependencyResolutionNode, constraints: [Constraint], availableLibraries: [LibraryMetadata] = []) throws -> (
201+
internal func solve(root: DependencyResolutionNode, constraints: [Constraint]) throws -> (
203202
bindings: [DependencyResolverBinding],
204203
state: State
205204
) {
206205
// first process inputs
207-
let inputs = try self.processInputs(root: root, with: constraints, availableLibraries: availableLibraries)
206+
let inputs = try self.processInputs(root: root, with: constraints)
208207

209208
// Prefetch the containers if prefetching is enabled.
210209
if self.prefetchBasedOnResolvedFile {
@@ -214,7 +213,7 @@ public struct PubGrubDependencyResolver {
214213
let pins = self.pins.values
215214
.map(\.packageRef)
216215
.filter { !inputs.overriddenPackages.keys.contains($0) }
217-
self.provider.prefetch(containers: pins, availableLibraries: availableLibraries)
216+
self.provider.prefetch(containers: pins, availableLibraries: self.availableLibraries)
218217
}
219218

220219
let state = State(root: root, overriddenPackages: inputs.overriddenPackages)
@@ -230,7 +229,7 @@ public struct PubGrubDependencyResolver {
230229
state.addIncompatibility(incompatibility, at: .topLevel)
231230
}
232231

233-
try self.run(state: state, availableLibraries: availableLibraries)
232+
try self.run(state: state)
234233

235234
let decisions = state.solution.assignments.filter(\.isDecision)
236235
var flattenedAssignments: [PackageReference: (binding: BoundVersion, products: ProductFilter)] = [:]
@@ -250,7 +249,7 @@ public struct PubGrubDependencyResolver {
250249
let products = assignment.term.node.productFilter
251250

252251
// TODO: replace with async/await when available
253-
let container = try temp_await { provider.getContainer(for: assignment.term.node.package, availableLibraries: availableLibraries, completion: $0) }
252+
let container = try temp_await { provider.getContainer(for: assignment.term.node.package, completion: $0) }
254253
let updatePackage = try container.underlying.loadPackageReference(at: boundVersion)
255254

256255
if var existing = flattenedAssignments[updatePackage] {
@@ -272,7 +271,7 @@ public struct PubGrubDependencyResolver {
272271
// Add overridden packages to the result.
273272
for (package, override) in state.overriddenPackages {
274273
// TODO: replace with async/await when available
275-
let container = try temp_await { provider.getContainer(for: package, availableLibraries: availableLibraries, completion: $0) }
274+
let container = try temp_await { provider.getContainer(for: package, completion: $0) }
276275
let updatePackage = try container.underlying.loadPackageReference(at: override.version)
277276
finalAssignments.append(.init(
278277
package: updatePackage,
@@ -288,8 +287,7 @@ public struct PubGrubDependencyResolver {
288287

289288
private func processInputs(
290289
root: DependencyResolutionNode,
291-
with constraints: [Constraint],
292-
availableLibraries: [LibraryMetadata]
290+
with constraints: [Constraint]
293291
) throws -> (
294292
overriddenPackages: [PackageReference: (version: BoundVersion, products: ProductFilter)],
295293
rootIncompatibilities: [Incompatibility]
@@ -331,10 +329,10 @@ public struct PubGrubDependencyResolver {
331329
// Process dependencies of this package.
332330
//
333331
// We collect all version-based dependencies in a separate structure so they can
334-
// be process at the end. This allows us to override them when there is a non-version
332+
// be processed at the end. This allows us to override them when there is a non-version
335333
// based (unversioned/branch-based) constraint present in the graph.
336334
// TODO: replace with async/await when available
337-
let container = try temp_await { provider.getContainer(for: node.package, availableLibraries: availableLibraries, completion: $0) }
335+
let container = try temp_await { provider.getContainer(for: node.package, completion: $0) }
338336
for dependency in try container.underlying.getUnversionedDependencies(productFilter: node.productFilter) {
339337
if let versionedBasedConstraints = VersionBasedConstraint.constraints(dependency) {
340338
for constraint in versionedBasedConstraints {
@@ -382,7 +380,7 @@ public struct PubGrubDependencyResolver {
382380
// Process dependencies of this package, similar to the first phase but branch-based dependencies
383381
// are not allowed to contain local/unversioned packages.
384382
// TODO: replace with async/await when avail
385-
let container = try temp_await { provider.getContainer(for: package, availableLibraries: availableLibraries, completion: $0) }
383+
let container = try temp_await { provider.getContainer(for: package, completion: $0) }
386384

387385
// If there is a pin for this revision-based dependency, get
388386
// the dependencies at the pinned revision instead of using
@@ -465,7 +463,7 @@ public struct PubGrubDependencyResolver {
465463
/// decisions if nothing else is left to be done.
466464
/// After this method returns `solution` is either populated with a list of
467465
/// final version assignments or an error is thrown.
468-
private func run(state: State, availableLibraries: [LibraryMetadata]) throws {
466+
private func run(state: State) throws {
469467
var next: DependencyResolutionNode? = state.root
470468

471469
while let nxt = next {
@@ -474,13 +472,13 @@ public struct PubGrubDependencyResolver {
474472
// initiate prefetch of known packages that will be used to make the decision on the next step
475473
self.provider.prefetch(
476474
containers: state.solution.undecided.map(\.node.package),
477-
availableLibraries: availableLibraries
475+
availableLibraries: self.availableLibraries
478476
)
479477

480478
// If decision making determines that no more decisions are to be
481479
// made, it returns nil to signal that version solving is done.
482480
// TODO: replace with async/await when available
483-
next = try temp_await { self.makeDecision(state: state, availableLibraries: availableLibraries, completion: $0) }
481+
next = try temp_await { self.makeDecision(state: state, completion: $0) }
484482
}
485483
}
486484

@@ -658,7 +656,6 @@ public struct PubGrubDependencyResolver {
658656

659657
private func computeCounts(
660658
for terms: [Term],
661-
availableLibraries: [LibraryMetadata],
662659
completion: @escaping (Result<[Term: Int], Error>) -> Void
663660
) {
664661
if terms.isEmpty {
@@ -670,7 +667,7 @@ public struct PubGrubDependencyResolver {
670667

671668
terms.forEach { term in
672669
sync.enter()
673-
provider.getContainer(for: term.node.package, availableLibraries: availableLibraries) { result in
670+
provider.getContainer(for: term.node.package) { result in
674671
defer { sync.leave() }
675672
results[term] = result.flatMap { container in Result(catching: { try container.versionCount(term.requirement) }) }
676673
}
@@ -687,7 +684,6 @@ public struct PubGrubDependencyResolver {
687684

688685
internal func makeDecision(
689686
state: State,
690-
availableLibraries: [LibraryMetadata] = [],
691687
completion: @escaping (Result<DependencyResolutionNode?, Error>) -> Void
692688
) {
693689
// If there are no more undecided terms, version solving is complete.
@@ -696,9 +692,33 @@ public struct PubGrubDependencyResolver {
696692
return completion(.success(nil))
697693
}
698694

695+
// If prebuilt libraries are available, let's attempt their versions first before going for
696+
// the latest viable version in the package. This way we archive multiple goals - prioritize
697+
// prebuilt libraries if they satisfy all requirements, avoid counting and building package
698+
// manifests and avoid (re-)building packages.
699+
//
700+
// Since the conflict resolution learns from incorrect terms this wouldn't be re-attempted.
701+
if !self.availableLibraries.isEmpty {
702+
let start = DispatchTime.now()
703+
for pkgTerm in undecided {
704+
let package = pkgTerm.node.package
705+
guard let library = package.matchingPrebuiltLibrary(in: self.availableLibraries) else {
706+
continue
707+
}
708+
709+
let version = Version(stringLiteral: library.version)
710+
711+
if pkgTerm.requirement.contains(version) {
712+
self.delegate?.didResolve(term: pkgTerm, version: version, duration: start.distance(to: .now()))
713+
state.decide(pkgTerm.node, at: version)
714+
return completion(.success(pkgTerm.node))
715+
}
716+
}
717+
}
718+
699719
// Prefer packages with least number of versions that fit the current requirements so we
700720
// get conflicts (if any) sooner.
701-
self.computeCounts(for: undecided, availableLibraries: availableLibraries) { result in
721+
self.computeCounts(for: undecided) { result in
702722
do {
703723
let start = DispatchTime.now()
704724
let counts = try result.get()
@@ -762,16 +782,13 @@ public extension PubGrubDependencyResolver {
762782
enum PubgrubError: Swift.Error, CustomStringConvertible {
763783
case _unresolvable(Incompatibility, [DependencyResolutionNode: [Incompatibility]])
764784
case unresolvable(String)
765-
case potentiallyUnresovableDueToPrebuiltLibrary([PackageReference], String)
766785

767786
public var description: String {
768787
switch self {
769788
case ._unresolvable(let rootCause, _):
770789
return rootCause.description
771790
case .unresolvable(let error):
772791
return error
773-
case .potentiallyUnresovableDueToPrebuiltLibrary(_, let error):
774-
return error
775792
}
776793
}
777794

@@ -781,8 +798,6 @@ public extension PubGrubDependencyResolver {
781798
return rootCause
782799
case .unresolvable:
783800
return nil
784-
case .potentiallyUnresovableDueToPrebuiltLibrary:
785-
return nil
786801
}
787802
}
788803

@@ -792,8 +807,6 @@ public extension PubGrubDependencyResolver {
792807
return incompatibilities
793808
case .unresolvable:
794809
return nil
795-
case .potentiallyUnresovableDueToPrebuiltLibrary:
796-
return nil
797810
}
798811
}
799812
}

Sources/SPMTestSupport/Resolver.swift

Lines changed: 0 additions & 19 deletions
This file was deleted.

Sources/Workspace/ResolverPrecomputationProvider.swift

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,14 @@ struct ResolverPrecomputationProvider: PackageContainerProvider {
4444
/// The tools version currently in use.
4545
let currentToolsVersion: ToolsVersion
4646

47-
/// The available libraries in the SDK.
48-
let availableLibraries: [LibraryMetadata]
49-
5047
init(
5148
root: PackageGraphRoot,
5249
dependencyManifests: Workspace.DependencyManifests,
53-
currentToolsVersion: ToolsVersion = ToolsVersion.current,
54-
availableLibraries: [LibraryMetadata]
50+
currentToolsVersion: ToolsVersion = ToolsVersion.current
5551
) {
5652
self.root = root
5753
self.dependencyManifests = dependencyManifests
5854
self.currentToolsVersion = currentToolsVersion
59-
self.availableLibraries = availableLibraries
6055
}
6156

6257
func getContainer(
@@ -89,15 +84,6 @@ struct ResolverPrecomputationProvider: PackageContainerProvider {
8984
return completion(.success(container))
9085
}
9186

92-
// Match against available prebuilt libraries.
93-
if let matchingPrebuiltLibrary = package.matchingPrebuiltLibrary(in: availableLibraries) {
94-
do {
95-
return completion(.success(try PrebuiltPackageContainer(metadata: matchingPrebuiltLibrary)))
96-
} catch {
97-
return completion(.failure(error))
98-
}
99-
}
100-
10187
// As we don't have anything else locally, error out.
10288
completion(.failure(ResolverPrecomputationError.missingPackage(package: package)))
10389
}

0 commit comments

Comments
 (0)