10
10
11
11
import struct PackageDescription. Version
12
12
13
+ public enum DependencyResolverError : Error {
14
+ /// The resolver was unable to find a solution to the input constraints.
15
+ case unsatisfiable
16
+
17
+ /// The resolver hit unimplemented functionality (used temporarily for test case coverage).
18
+ //
19
+ // FIXME: Eliminate this.
20
+ case unimplemented
21
+ }
22
+
13
23
/// An abstract definition for a set of versions.
14
24
public enum VersionSetSpecifier : Equatable {
15
25
/// The universal set.
@@ -207,6 +217,11 @@ struct PackageContainerConstraintSet<C: PackageContainer>: Collection {
207
217
self . constraints = [ : ]
208
218
}
209
219
220
+ /// Create an constraint set from known values.
221
+ init ( _ constraints: [ Identifier : VersionSetSpecifier ] ) {
222
+ self . constraints = constraints
223
+ }
224
+
210
225
/// The list of containers with entries in the set.
211
226
var containerIdentifiers : AnySequence < Identifier > {
212
227
return AnySequence < C . Identifier > ( constraints. keys)
@@ -509,6 +524,11 @@ public class DependencyResolver<
509
524
/// only kind of constraints we operate on.
510
525
public typealias Constraint = PackageContainerConstraint < Identifier >
511
526
527
+ /// The type of constraint set the resolver operates on.
528
+ typealias ConstraintSet = PackageContainerConstraintSet < Container >
529
+
530
+ /// The type of assignment the resolver operates on.
531
+ typealias AssignmentSet = VersionAssignmentSet < Container >
512
532
513
533
/// The container provider used to load package containers.
514
534
public let provider : Provider
@@ -526,37 +546,161 @@ public class DependencyResolver<
526
546
/// - Parameters:
527
547
/// - constraints: The contraints to solve.
528
548
/// - Returns: A satisfying assignment of containers and versions.
549
+ /// - Throws: DependencyResolverError, or errors from the underlying package provider.
529
550
public func resolve( constraints: [ Constraint ] ) throws -> [ ( container: Identifier , version: Version ) ] {
530
- // For now, we just load the transitive closure of the dependencies at
531
- // the latest version, and ignore the version requirements.
551
+ // Create an assignment for the input constraints.
552
+ guard let assignment = try merge (
553
+ constraints: constraints, into: AssignmentSet ( ) ,
554
+ subjectTo: ConstraintSet ( ) , excluding: [ : ] ) else {
555
+ throw DependencyResolverError . unsatisfiable
556
+ }
532
557
533
- func visit( _ identifier: Identifier ) throws {
534
- // If we already have this identifier, skip it.
535
- if containers. keys. contains ( identifier) {
536
- return
558
+ return assignment. map { ( container, binding) in
559
+ guard case . version( let version) = binding else {
560
+ fatalError ( " unexpected exclude binding " )
537
561
}
562
+ return ( container: container. identifier, version: version)
563
+ }
564
+ }
538
565
539
- // Otherwise, load the container and visit its dependencies.
540
- let container = try getContainer ( for: identifier)
566
+ /// Resolve an individual container dependency tree.
567
+ ///
568
+ /// This is the primary method in our bottom-up algorithm for resolving
569
+ /// dependencies. The inputs define an active set of constraints and set of
570
+ /// versions to exclude (conceptually the latter could be merged with the
571
+ /// former, but it is convenient to separate them in our
572
+ /// implementation). The result is an assignment for this container's
573
+ /// subtree.
574
+ ///
575
+ /// - Parameters:
576
+ /// - container: The container to resolve.
577
+ /// - constraints: The external constraints which must be honored by the solution.
578
+ /// - exclusions: The list of individually excluded package versions.
579
+ /// - Returns: A sequence of feasible solutions, starting with the most preferable.
580
+ /// - Throws: Only rethrows errors from the container provider.
581
+ //
582
+ // FIXME: This needs to a way to return information on the failure, or we
583
+ // will need to have it call the delegate directly.
584
+ //
585
+ // FIXME: @testable private
586
+ func resolveSubtree(
587
+ _ container: Container ,
588
+ subjectTo allConstraints: ConstraintSet ,
589
+ excluding allExclusions: [ Identifier : Set < Version > ]
590
+ ) throws -> AssignmentSet ? {
591
+ func validVersions( _ container: Container ) -> AnyIterator < Version > {
592
+ let constraints = allConstraints [ container. identifier] ?? . any
593
+ let exclusions = allExclusions [ container. identifier] ?? Set ( )
594
+ var it = container. versions. reversed ( ) . makeIterator ( )
595
+ return AnyIterator { ( ) -> Version ? in
596
+ while let version = it. next ( ) {
597
+ if constraints. contains ( version) && !exclusions. contains ( version) {
598
+ return version
599
+ }
600
+ }
601
+ return nil
602
+ }
603
+ }
541
604
542
- // Visit the dependencies at the latest version.
605
+ // Attempt to select each valid version in order.
606
+ //
607
+ // FIXME: We must detect recursion here.
608
+ for version in validVersions ( container) {
609
+ // Create local constaint copies we will use to build the solution.
610
+ var allConstraints = allConstraints
611
+ // FIXME: We need a persistent set data structure for this to be efficient.
612
+ let allExclusions = allExclusions
613
+
614
+ // Create an assignment for this container.
615
+ var assignment = AssignmentSet ( )
616
+ assignment [ container] = . version( version)
617
+
618
+ // Update the active constraint set to include this container's constraints.
619
+ //
620
+ // We want to put all of these constraints in up front so that we
621
+ // are more likely to get back a viable solution.
543
622
//
544
- // FIXME: What if this dependency has no versions? We should
545
- // consider it unavailable.
546
- let latestVersion = container. versions. last!
547
- let constraints = container. getDependencies ( at: latestVersion)
623
+ // FIXME: We should have a test for this, probably by adding some
624
+ // kind of statistics on the number of backtracks.
625
+ guard allConstraints. merge ( assignment. constraints) else {
626
+ // The constraints themselves were unsatisfiable after merging, so the version is invalid.
627
+ continue
628
+ }
548
629
549
- for constraint in constraints {
550
- try visit ( constraint. identifier)
630
+ // Get the constraints for this container version and update the
631
+ // assignment to include each one.
632
+ if let result = try merge (
633
+ constraints: container. getDependencies ( at: version) ,
634
+ into: assignment, subjectTo: allConstraints, excluding: allExclusions) {
635
+ // We found a complete valid assignment.
636
+ assert ( result. checkIfValidAndComplete ( ) )
637
+ return result
551
638
}
552
639
}
640
+
641
+ // We were unable to find a valid solution.
642
+ return nil
643
+ }
644
+
645
+ /// Solve the `constraints` and merge the results into the `assignment`.
646
+ ///
647
+ /// - Parameters:
648
+ /// - constraints: The input list of constraints to solve.
649
+ /// - assignment: The assignment to merge the result into.
650
+ /// - allConstraints: An additional set of constraints on the viable solutions.
651
+ /// - allExclusions: A set of package assignments to exclude from consideration.
652
+ /// - Returns: A satisfying assignment, if solvable.
653
+ private func merge(
654
+ constraints: [ Constraint ] ,
655
+ into assignment: AssignmentSet ,
656
+ subjectTo allConstraints: ConstraintSet ,
657
+ excluding allExclusions: [ Identifier : Set < Version > ]
658
+ ) throws -> AssignmentSet ? {
659
+ var assignment = assignment
660
+ var allConstraints = allConstraints
661
+
553
662
for constraint in constraints {
554
- try visit ( constraint. identifier)
555
- }
663
+ // Get the container.
664
+ //
665
+ // Failures here will immediately abort the solution, although in
666
+ // theory one could imagine attempting to find a solution not
667
+ // requiring this container. It isn't clear that is something we
668
+ // would ever want to handle at this level.
669
+ let container = try getContainer ( for: constraint. identifier)
670
+
671
+ // Solve for an assignment with the current constraints.
672
+ guard let subtreeAssignment = try resolveSubtree (
673
+ container, subjectTo: allConstraints, excluding: allExclusions) else {
674
+ // If we couldn't find an assignment, we need to backtrack in some way.
675
+ throw DependencyResolverError . unimplemented
676
+ }
556
677
557
- return containers. map { ( identifier, container) in
558
- return ( container: identifier, version: container. versions. last!)
678
+ // We found a valid assignment, attempt to merge it with the current solution.
679
+ //
680
+ // FIXME: It is rather important, subtle, and confusing that this
681
+ // `merge` doesn't mutate the assignment but the one on the
682
+ // constraint set does. We should probably make them consistent.
683
+ guard assignment. merge ( subtreeAssignment) else {
684
+ // The assignment couldn't be merged with the current
685
+ // assignment, or the constraint sets couldn't be merged.
686
+ //
687
+ // This happens when (a) the subtree has a package overlapping
688
+ // with a previous subtree assignment, and (b) the subtrees
689
+ // needed to resolve different versions due to constraints not
690
+ // present in the top-down constraint set.
691
+ throw DependencyResolverError . unimplemented
692
+ }
693
+
694
+ // Merge the working constraint set.
695
+ //
696
+ // This should always be feasible, because all prior constraints
697
+ // were part of the input constraint request (see comment around
698
+ // initial `merge` outside the loop).
699
+ let mergable = allConstraints. merge ( subtreeAssignment. constraints)
700
+ precondition ( mergable)
559
701
}
702
+
703
+ return assignment
560
704
}
561
705
562
706
// MARK: Container Management
0 commit comments