This repository has been archived by the owner on Feb 3, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 22
/
solver.go
1245 lines (1090 loc) · 37.7 KB
/
solver.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package gps
import (
"container/heap"
"fmt"
"log"
"sort"
"strings"
"github.com/armon/go-radix"
"github.com/sdboyer/gps/internal"
"github.com/sdboyer/gps/pkgtree"
)
var (
osList []string
archList []string
ignoreTags = []string{} //[]string{"appengine", "ignore"} //TODO: appengine is a special case for now: https://github.com/tools/godep/issues/353
)
func init() {
// The supported systems are listed in
// https://github.com/golang/go/blob/master/src/go/build/syslist.go
// The lists are not exported, so we need to duplicate them here.
osListString := "android darwin dragonfly freebsd linux nacl netbsd openbsd plan9 solaris windows"
osList = strings.Split(osListString, " ")
archListString := "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le mips mipsle mips64 mips64le mips64p32 mips64p32le ppc s390 s390x sparc sparc64"
archList = strings.Split(archListString, " ")
}
var rootRev = Revision("")
// SolveParameters hold all arguments to a solver run.
//
// Only RootDir and RootPackageTree are absolutely required. A nil Manifest is
// allowed, though it usually makes little sense.
//
// Of these properties, only the Manifest and RootPackageTree are (directly)
// incorporated in memoization hashing.
type SolveParameters struct {
// The path to the root of the project on which the solver should operate.
// This should point to the directory that should contain the vendor/
// directory.
//
// In general, it is wise for this to be under an active GOPATH, though it
// is not (currently) required.
//
// A real path to a readable directory is required.
RootDir string
// The ProjectAnalyzer is responsible for extracting Manifest and
// (optionally) Lock information from dependencies. The solver passes it
// along to its SourceManager's GetManifestAndLock() method as needed.
//
// An analyzer is required.
ProjectAnalyzer ProjectAnalyzer
// The tree of packages that comprise the root project, as well as the
// import path that should identify the root of that tree.
//
// In most situations, tools should simply pass the result of ListPackages()
// directly through here.
//
// The ImportRoot property must be a non-empty string, and at least one
// element must be present in the Packages map.
RootPackageTree pkgtree.PackageTree
// The root manifest. This contains all the dependency constraints
// associated with normal Manifests, as well as the particular controls
// afforded only to the root project.
//
// May be nil, but for most cases, that would be unwise.
Manifest RootManifest
// The root lock. Optional. Generally, this lock is the output of a previous
// solve run.
//
// If provided, the solver will attempt to preserve the versions specified
// in the lock, unless ToChange or ChangeAll settings indicate otherwise.
Lock Lock
// ToChange is a list of project names that should be changed - that is, any
// versions specified for those projects in the root lock file should be
// ignored.
//
// Passing ChangeAll has subtly different behavior from enumerating all
// projects into ToChange. In general, ToChange should *only* be used if the
// user expressly requested an upgrade for a specific project.
ToChange []ProjectRoot
// ChangeAll indicates that all projects should be changed - that is, any
// versions specified in the root lock file should be ignored.
ChangeAll bool
// Downgrade indicates whether the solver will attempt to upgrade (false) or
// downgrade (true) projects that are not locked, or are marked for change.
//
// Upgrading is, by far, the most typical case. The field is named
// 'Downgrade' so that the bool's zero value corresponds to that most
// typical case.
Downgrade bool
// Trace controls whether the solver will generate informative trace output
// as it moves through the solving process.
Trace bool
// TraceLogger is the logger to use for generating trace output. If Trace is
// true but no logger is provided, solving will result in an error.
TraceLogger *log.Logger
}
// solver is a CDCL-style constraint solver with satisfiability conditions
// hardcoded to the needs of the Go package management problem space.
type solver struct {
// The current number of attempts made over the course of this solve. This
// number increments each time the algorithm completes a backtrack and
// starts moving forward again.
attempts int
// Logger used exclusively for trace output, if the trace option is set.
tl *log.Logger
// A bridge to the standard SourceManager. The adapter does some local
// caching of pre-sorted version lists, as well as translation between the
// full-on ProjectIdentifiers that the solver deals with and the simplified
// names a SourceManager operates on.
b sourceBridge
// A versionUnifier, to facilitate cross-type version comparison and set
// operations.
vUnify versionUnifier
// A stack containing projects and packages that are currently "selected" -
// that is, they have passed all satisfiability checks, and are part of the
// current solution.
//
// The *selection type is mostly just a dumb data container; the solver
// itself is responsible for maintaining that invariant.
sel *selection
// The current list of projects that we need to incorporate into the solution in
// order for the solution to be complete. This list is implemented as a
// priority queue that places projects least likely to induce errors at the
// front, in order to minimize the amount of backtracking required to find a
// solution.
//
// Entries are added to and removed from this list by the solver at the same
// time that the selected queue is updated, either with an addition or
// removal.
unsel *unselected
// A stack of all the currently active versionQueues in the solver. The set
// of projects represented here corresponds closely to what's in s.sel,
// although s.sel will always contain the root project, and s.vqs never
// will. Also, s.vqs is only added to (or popped from during backtracking)
// when a new project is selected; it is untouched when new packages are
// added to an existing project.
vqs []*versionQueue
// Contains data and constraining information from the root project
rd rootdata
// metrics for the current solve run.
mtr *metrics
}
func (params SolveParameters) toRootdata() (rootdata, error) {
if params.ProjectAnalyzer == nil {
return rootdata{}, badOptsFailure("must provide a ProjectAnalyzer")
}
if params.RootDir == "" {
return rootdata{}, badOptsFailure("params must specify a non-empty root directory")
}
if params.RootPackageTree.ImportRoot == "" {
return rootdata{}, badOptsFailure("params must include a non-empty import root")
}
if len(params.RootPackageTree.Packages) == 0 {
return rootdata{}, badOptsFailure("at least one package must be present in the PackageTree")
}
if params.Lock == nil && len(params.ToChange) != 0 {
return rootdata{}, badOptsFailure(fmt.Sprintf("update specifically requested for %s, but no lock was provided to upgrade from", params.ToChange))
}
if params.Manifest == nil {
params.Manifest = simpleRootManifest{}
}
rd := rootdata{
ig: params.Manifest.IgnoredPackages(),
req: params.Manifest.RequiredPackages(),
ovr: params.Manifest.Overrides(),
rpt: params.RootPackageTree.Copy(),
chng: make(map[ProjectRoot]struct{}),
rlm: make(map[ProjectRoot]LockedProject),
chngall: params.ChangeAll,
dir: params.RootDir,
an: params.ProjectAnalyzer,
}
// Ensure the required, ignore and overrides maps are at least initialized
if rd.ig == nil {
rd.ig = make(map[string]bool)
}
if rd.req == nil {
rd.req = make(map[string]bool)
}
if rd.ovr == nil {
rd.ovr = make(ProjectConstraints)
}
if len(rd.ig) != 0 {
var both []string
for pkg := range params.Manifest.RequiredPackages() {
if rd.ig[pkg] {
both = append(both, pkg)
}
}
switch len(both) {
case 0:
break
case 1:
return rootdata{}, badOptsFailure(fmt.Sprintf("%q was given as both a required and ignored package", both[0]))
default:
return rootdata{}, badOptsFailure(fmt.Sprintf("multiple packages given as both required and ignored: %s", strings.Join(both, ", ")))
}
}
// Validate no empties in the overrides map
var eovr []string
for pr, pp := range rd.ovr {
if pp.Constraint == nil && pp.Source == "" {
eovr = append(eovr, string(pr))
}
}
if eovr != nil {
// Maybe it's a little nitpicky to do this (we COULD proceed; empty
// overrides have no effect), but this errs on the side of letting the
// tool/user know there's bad input. Purely as a principle, that seems
// preferable to silently allowing progress with icky input.
if len(eovr) > 1 {
return rootdata{}, badOptsFailure(fmt.Sprintf("Overrides lacked any non-zero properties for multiple project roots: %s", strings.Join(eovr, " ")))
}
return rootdata{}, badOptsFailure(fmt.Sprintf("An override was declared for %s, but without any non-zero properties", eovr[0]))
}
// Prep safe, normalized versions of root manifest and lock data
rd.rm = prepManifest(params.Manifest)
if params.Lock != nil {
for _, lp := range params.Lock.Projects() {
rd.rlm[lp.Ident().ProjectRoot] = lp
}
// Also keep a prepped one, mostly for the bridge. This is probably
// wasteful, but only minimally so, and yay symmetry
rd.rl = prepLock(params.Lock)
}
for _, p := range params.ToChange {
if _, exists := rd.rlm[p]; !exists {
return rootdata{}, badOptsFailure(fmt.Sprintf("cannot update %s as it is not in the lock", p))
}
rd.chng[p] = struct{}{}
}
return rd, nil
}
// Prepare readies a Solver for use.
//
// This function reads and validates the provided SolveParameters. If a problem
// with the inputs is detected, an error is returned. Otherwise, a Solver is
// returned, ready to hash and check inputs or perform a solving run.
func Prepare(params SolveParameters, sm SourceManager) (Solver, error) {
if sm == nil {
return nil, badOptsFailure("must provide non-nil SourceManager")
}
if params.Trace && params.TraceLogger == nil {
return nil, badOptsFailure("trace requested, but no logger provided")
}
rd, err := params.toRootdata()
if err != nil {
return nil, err
}
s := &solver{
tl: params.TraceLogger,
rd: rd,
}
// Set up the bridge and ensure the root dir is in good, working order
// before doing anything else. (This call is stubbed out in tests, via
// overriding mkBridge(), so we can run with virtual RootDir.)
s.b = mkBridge(s, sm, params.Downgrade)
err = s.b.verifyRootDir(params.RootDir)
if err != nil {
return nil, err
}
s.vUnify = versionUnifier{
b: s.b,
}
// Initialize stacks and queues
s.sel = &selection{
deps: make(map[ProjectRoot][]dependency),
vu: s.vUnify,
}
s.unsel = &unselected{
sl: make([]bimodalIdentifier, 0),
cmp: s.unselectedComparator,
}
return s, nil
}
// A Solver is the main workhorse of gps: given a set of project inputs, it
// performs a constraint solving analysis to develop a complete Solution, or
// else fail with an informative error.
//
// If a Solution is found, an implementing tool may persist it - typically into
// a "lock file" - and/or use it to write out a directory tree of dependencies,
// suitable to be a vendor directory, via CreateVendorTree.
type Solver interface {
// HashInputs hashes the unique inputs to this solver, returning the hash
// digest. It is guaranteed that, if the resulting digest is equal to the
// digest returned from a previous Solution.InputHash(), that that Solution
// is valid for this Solver's inputs.
//
// In such a case, it may not be necessary to run Solve() at all.
HashInputs() []byte
// Solve initiates a solving run. It will either complete successfully with
// a Solution, or fail with an informative error.
Solve() (Solution, error)
}
// Solve attempts to find a dependency solution for the given project, as
// represented by the SolveParameters with which this Solver was created.
//
// This is the entry point to the main gps workhorse.
func (s *solver) Solve() (Solution, error) {
// Set up a metrics object
s.mtr = newMetrics()
s.vUnify.mtr = s.mtr
// Prime the queues with the root project
err := s.selectRoot()
if err != nil {
return nil, err
}
all, err := s.solve()
s.mtr.pop()
var soln solution
if err == nil {
soln = solution{
att: s.attempts,
}
soln.hd = s.HashInputs()
// Convert ProjectAtoms into LockedProjects
soln.p = make([]LockedProject, len(all))
k := 0
for pa, pl := range all {
soln.p[k] = pa2lp(pa, pl)
k++
}
}
s.traceFinish(soln, err)
if s.tl != nil {
s.mtr.dump(s.tl)
}
return soln, err
}
// solve is the top-level loop for the solving process.
func (s *solver) solve() (map[atom]map[string]struct{}, error) {
// Main solving loop
for {
bmi, has := s.nextUnselected()
if !has {
// no more packages to select - we're done.
break
}
// This split is the heart of "bimodal solving": we follow different
// satisfiability and selection paths depending on whether we've already
// selected the base project/repo that came off the unselected queue.
//
// (If we've already selected the project, other parts of the algorithm
// guarantee the bmi will contain at least one package from this project
// that has yet to be selected.)
if awp, is := s.sel.selected(bmi.id); !is {
s.mtr.push("new-atom")
// Analysis path for when we haven't selected the project yet - need
// to create a version queue.
queue, err := s.createVersionQueue(bmi)
if err != nil {
// Err means a failure somewhere down the line; try backtracking.
s.traceStartBacktrack(bmi, err, false)
s.mtr.pop()
if s.backtrack() {
// backtracking succeeded, move to the next unselected id
continue
}
return nil, err
}
if queue.current() == nil {
panic("canary - queue is empty, but flow indicates success")
}
awp := atomWithPackages{
a: atom{
id: queue.id,
v: queue.current(),
},
pl: bmi.pl,
}
s.selectAtom(awp, false)
s.vqs = append(s.vqs, queue)
s.mtr.pop()
} else {
s.mtr.push("add-atom")
// We're just trying to add packages to an already-selected project.
// That means it's not OK to burn through the version queue for that
// project as we do when first selecting a project, as doing so
// would upend the guarantees on which all previous selections of
// the project are based (both the initial one, and any package-only
// ones).
// Because we can only safely operate within the scope of the
// single, currently selected version, we can skip looking for the
// queue and just use the version given in what came back from
// s.sel.selected().
nawp := atomWithPackages{
a: atom{
id: bmi.id,
v: awp.a.v,
},
pl: bmi.pl,
}
s.traceCheckPkgs(bmi)
err := s.check(nawp, true)
if err != nil {
// Err means a failure somewhere down the line; try backtracking.
s.traceStartBacktrack(bmi, err, true)
if s.backtrack() {
// backtracking succeeded, move to the next unselected id
continue
}
s.mtr.pop()
return nil, err
}
s.selectAtom(nawp, true)
// We don't add anything to the stack of version queues because the
// backtracker knows not to pop the vqstack if it backtracks
// across a pure-package addition.
s.mtr.pop()
}
}
// Getting this far means we successfully found a solution. Combine the
// selected projects and packages.
projs := make(map[atom]map[string]struct{})
// Skip the first project. It's always the root, and that shouldn't be
// included in results.
for _, sel := range s.sel.projects[1:] {
pm, exists := projs[sel.a.a]
if !exists {
pm = make(map[string]struct{})
projs[sel.a.a] = pm
}
for _, path := range sel.a.pl {
pm[path] = struct{}{}
}
}
return projs, nil
}
// selectRoot is a specialized selectAtom, used solely to initially
// populate the queues at the beginning of a solve run.
func (s *solver) selectRoot() error {
s.mtr.push("select-root")
// Push the root project onto the queue.
awp := s.rd.rootAtom()
s.sel.pushSelection(awp, true)
// If we're looking for root's deps, get it from opts and local root
// analysis, rather than having the sm do it
deps, err := s.intersectConstraintsWithImports(s.rd.combineConstraints(), s.rd.externalImportList())
if err != nil {
// TODO(sdboyer) this could well happen; handle it with a more graceful error
panic(fmt.Sprintf("shouldn't be possible %s", err))
}
for _, dep := range deps {
// If we have no lock, or if this dep isn't in the lock, then prefetch
// it. See longer explanation in selectAtom() for how we benefit from
// parallelism here.
if s.rd.needVersionsFor(dep.Ident.ProjectRoot) {
go s.b.SyncSourceFor(dep.Ident)
}
s.sel.pushDep(dependency{depender: awp.a, dep: dep})
// Add all to unselected queue
heap.Push(s.unsel, bimodalIdentifier{id: dep.Ident, pl: dep.pl, fromRoot: true})
}
s.traceSelectRoot(s.rd.rpt, deps)
s.mtr.pop()
return nil
}
func (s *solver) getImportsAndConstraintsOf(a atomWithPackages) ([]string, []completeDep, error) {
var err error
if s.rd.isRoot(a.a.id.ProjectRoot) {
panic("Should never need to recheck imports/constraints from root during solve")
}
// Work through the source manager to get project info and static analysis
// information.
m, _, err := s.b.GetManifestAndLock(a.a.id, a.a.v, s.rd.an)
if err != nil {
return nil, nil, err
}
ptree, err := s.b.ListPackages(a.a.id, a.a.v)
if err != nil {
return nil, nil, err
}
rm, em := ptree.ToReachMap(true, false, true, s.rd.ig)
// Use maps to dedupe the unique internal and external packages.
exmap, inmap := make(map[string]struct{}), make(map[string]struct{})
for _, pkg := range a.pl {
inmap[pkg] = struct{}{}
for _, ipkg := range rm[pkg].Internal {
inmap[ipkg] = struct{}{}
}
}
var pl []string
// If lens are the same, then the map must have the same contents as the
// slice; no need to build a new one.
if len(inmap) == len(a.pl) {
pl = a.pl
} else {
pl = make([]string, 0, len(inmap))
for pkg := range inmap {
pl = append(pl, pkg)
}
sort.Strings(pl)
}
// Add to the list those packages that are reached by the packages
// explicitly listed in the atom
for _, pkg := range a.pl {
// Skip ignored packages
if s.rd.ig[pkg] {
continue
}
ie, exists := rm[pkg]
if !exists {
// Missing package here *should* only happen if the target pkg was
// poisoned. Check the errors map
if importErr, eexists := em[pkg]; eexists {
return nil, nil, importErr
}
// Nope, it's actually full-on not there.
return nil, nil, fmt.Errorf("package %s does not exist within project %s", pkg, a.a.id.errString())
}
for _, ex := range ie.External {
exmap[ex] = struct{}{}
}
}
reach := make([]string, 0, len(exmap))
for pkg := range exmap {
reach = append(reach, pkg)
}
sort.Strings(reach)
deps := s.rd.ovr.overrideAll(m.DependencyConstraints())
cd, err := s.intersectConstraintsWithImports(deps, reach)
return pl, cd, err
}
// intersectConstraintsWithImports takes a list of constraints and a list of
// externally reached packages, and creates a []completeDep that is guaranteed
// to include all packages named by import reach, using constraints where they
// are available, or Any() where they are not.
func (s *solver) intersectConstraintsWithImports(deps []workingConstraint, reach []string) ([]completeDep, error) {
// Create a radix tree with all the projects we know from the manifest
xt := radix.New()
for _, dep := range deps {
xt.Insert(string(dep.Ident.ProjectRoot), dep)
}
// Step through the reached packages; if they have prefix matches in
// the trie, assume (mostly) it's a correct correspondence.
dmap := make(map[ProjectRoot]completeDep)
for _, rp := range reach {
// If it's a stdlib-shaped package, skip it.
if internal.IsStdLib(rp) {
continue
}
// Look for a prefix match; it'll be the root project/repo containing
// the reached package
if pre, idep, match := xt.LongestPrefix(rp); match && isPathPrefixOrEqual(pre, rp) {
// Match is valid; put it in the dmap, either creating a new
// completeDep or appending it to the existing one for this base
// project/prefix.
dep := idep.(workingConstraint)
if cdep, exists := dmap[dep.Ident.ProjectRoot]; exists {
cdep.pl = append(cdep.pl, rp)
dmap[dep.Ident.ProjectRoot] = cdep
} else {
dmap[dep.Ident.ProjectRoot] = completeDep{
workingConstraint: dep,
pl: []string{rp},
}
}
continue
}
// No match. Let the SourceManager try to figure out the root
root, err := s.b.DeduceProjectRoot(rp)
if err != nil {
// Nothing we can do if we can't suss out a root
return nil, err
}
// Make a new completeDep with an open constraint, respecting overrides
pd := s.rd.ovr.override(root, ProjectProperties{Constraint: Any()})
// Insert the pd into the trie so that further deps from this
// project get caught by the prefix search
xt.Insert(string(root), pd)
// And also put the complete dep into the dmap
dmap[root] = completeDep{
workingConstraint: pd,
pl: []string{rp},
}
}
// Dump all the deps from the map into the expected return slice
cdeps := make([]completeDep, len(dmap))
k := 0
for _, cdep := range dmap {
cdeps[k] = cdep
k++
}
return cdeps, nil
}
func (s *solver) createVersionQueue(bmi bimodalIdentifier) (*versionQueue, error) {
id := bmi.id
// If on the root package, there's no queue to make
if s.rd.isRoot(id.ProjectRoot) {
return newVersionQueue(id, nil, nil, s.b)
}
exists, err := s.b.SourceExists(id)
if err != nil {
return nil, err
}
if !exists {
exists, err = s.b.vendorCodeExists(id)
if err != nil {
return nil, err
}
if exists {
// Project exists only in vendor
// FIXME(sdboyer) this just totally doesn't work at all right now
} else {
return nil, fmt.Errorf("project '%s' could not be located", id)
}
}
var lockv Version
if len(s.rd.rlm) > 0 {
lockv, err = s.getLockVersionIfValid(id)
if err != nil {
// Can only get an error here if an upgrade was expressly requested on
// code that exists only in vendor
return nil, err
}
}
var prefv Version
if bmi.fromRoot {
// If this bmi came from the root, then we want to search through things
// with a dependency on it in order to see if any have a lock that might
// express a prefv
//
// TODO(sdboyer) nested loop; prime candidate for a cache somewhere
for _, dep := range s.sel.getDependenciesOn(bmi.id) {
// Skip the root, of course
if s.rd.isRoot(dep.depender.id.ProjectRoot) {
continue
}
_, l, err := s.b.GetManifestAndLock(dep.depender.id, dep.depender.v, s.rd.an)
if err != nil || l == nil {
// err being non-nil really shouldn't be possible, but the lock
// being nil is quite likely
continue
}
for _, lp := range l.Projects() {
if lp.Ident().eq(bmi.id) {
prefv = lp.Version()
}
}
}
// OTHER APPROACH - WRONG, BUT MAYBE USEFUL FOR REFERENCE?
// If this bmi came from the root, then we want to search the unselected
// queue to see if anything *else* wants this ident, in which case we
// pick up that prefv
//for _, bmi2 := range s.unsel.sl {
//// Take the first thing from the queue that's for the same ident,
//// and has a non-nil prefv
//if bmi.id.eq(bmi2.id) {
//if bmi2.prefv != nil {
//prefv = bmi2.prefv
//}
//}
//}
} else {
// Otherwise, just use the preferred version expressed in the bmi
prefv = bmi.prefv
}
q, err := newVersionQueue(id, lockv, prefv, s.b)
if err != nil {
// TODO(sdboyer) this particular err case needs to be improved to be ONLY for cases
// where there's absolutely nothing findable about a given project name
return nil, err
}
// Hack in support for revisions.
//
// By design, revs aren't returned from ListVersion(). Thus, if the dep in
// the bmi was has a rev constraint, it is (almost) guaranteed to fail, even
// if that rev does exist in the repo. So, detect a rev and push it into the
// vq here, instead.
//
// Happily, the solver maintains the invariant that constraints on a given
// ident cannot be incompatible, so we know that if we find one rev, then
// any other deps will have to also be on that rev (or Any).
//
// TODO(sdboyer) while this does work, it bypasses the interface-implied guarantees
// of the version queue, and is therefore not a great strategy for API
// coherency. Folding this in to a formal interface would be better.
switch tc := s.sel.getConstraint(bmi.id).(type) {
case Revision:
// We know this is the only thing that could possibly match, so put it
// in at the front - if it isn't there already.
if q.pi[0] != tc {
// Existence of the revision is guaranteed by checkRevisionExists().
q.pi = append([]Version{tc}, q.pi...)
}
}
// Having assembled the queue, search it for a valid version.
s.traceCheckQueue(q, bmi, false, 1)
return q, s.findValidVersion(q, bmi.pl)
}
// findValidVersion walks through a versionQueue until it finds a version that
// satisfies the constraints held in the current state of the solver.
//
// The satisfiability checks triggered from here are constrained to operate only
// on those dependencies induced by the list of packages given in the second
// parameter.
func (s *solver) findValidVersion(q *versionQueue, pl []string) error {
if nil == q.current() {
// this case should not be reachable, but reflects improper solver state
// if it is, so panic immediately
panic("version queue is empty, should not happen")
}
faillen := len(q.fails)
for {
cur := q.current()
s.traceInfo("try %s@%s", q.id.errString(), cur)
err := s.check(atomWithPackages{
a: atom{
id: q.id,
v: cur,
},
pl: pl,
}, false)
if err == nil {
// we have a good version, can return safely
return nil
}
if q.advance(err) != nil {
// Error on advance, have to bail out
break
}
if q.isExhausted() {
// Queue is empty, bail with error
break
}
}
s.fail(s.sel.getDependenciesOn(q.id)[0].depender.id)
// Return a compound error of all the new errors encountered during this
// attempt to find a new, valid version
return &noVersionError{
pn: q.id,
fails: q.fails[faillen:],
}
}
// getLockVersionIfValid finds an atom for the given ProjectIdentifier from the
// root lock, assuming:
//
// 1. A root lock was provided
// 2. The general flag to change all projects was not passed
// 3. A flag to change this particular ProjectIdentifier was not passed
//
// If any of these three conditions are true (or if the id cannot be found in
// the root lock), then no atom will be returned.
func (s *solver) getLockVersionIfValid(id ProjectIdentifier) (Version, error) {
// If the project is specifically marked for changes, then don't look for a
// locked version.
if _, explicit := s.rd.chng[id.ProjectRoot]; explicit || s.rd.chngall {
// For projects with an upstream or cache repository, it's safe to
// ignore what's in the lock, because there's presumably more versions
// to be found and attempted in the repository. If it's only in vendor,
// though, then we have to try to use what's in the lock, because that's
// the only version we'll be able to get.
if exist, _ := s.b.SourceExists(id); exist {
// Upgrades mean breaking the lock
s.b.breakLock()
return nil, nil
}
// However, if a change was *expressly* requested for something that
// exists only in vendor, then that guarantees we don't have enough
// information to complete a solution. In that case, error out.
if explicit {
return nil, &missingSourceFailure{
goal: id,
prob: "Cannot upgrade %s, as no source repository could be found.",
}
}
}
lp, exists := s.rd.rlm[id.ProjectRoot]
if !exists {
return nil, nil
}
constraint := s.sel.getConstraint(id)
v := lp.Version()
if !constraint.Matches(v) {
var found bool
if tv, ok := v.(Revision); ok {
// If we only have a revision from the root's lock, allow matching
// against other versions that have that revision
for _, pv := range s.vUnify.pairRevision(id, tv) {
if constraint.Matches(pv) {
v = pv
found = true
break
}
}
//} else if _, ok := constraint.(Revision); ok {
//// If the current constraint is itself a revision, and the lock gave
//// an unpaired version, see if they match up
////
//if u, ok := v.(UnpairedVersion); ok {
//pv := s.sm.pairVersion(id, u)
//if constraint.Matches(pv) {
//v = pv
//found = true
//}
//}
}
if !found {
// No match found, which means we're going to be breaking the lock
s.b.breakLock()
return nil, nil
}
}
return v, nil
}
// backtrack works backwards from the current failed solution to find the next
// solution to try.
func (s *solver) backtrack() bool {
if len(s.vqs) == 0 {
// nothing to backtrack to
return false
}
s.mtr.push("backtrack")
for {
for {
if len(s.vqs) == 0 {
// no more versions, nowhere further to backtrack
return false
}
if s.vqs[len(s.vqs)-1].failed {
break
}
s.vqs, s.vqs[len(s.vqs)-1] = s.vqs[:len(s.vqs)-1], nil
// Pop selections off until we get to a project.
var proj bool
var awp atomWithPackages
for !proj {
awp, proj = s.unselectLast()
s.traceBacktrack(awp.bmi(), !proj)
}
}
// Grab the last versionQueue off the list of queues
q := s.vqs[len(s.vqs)-1]
// Walk back to the next project
awp, proj := s.unselectLast()
if !proj {
panic("canary - *should* be impossible to have a pkg-only selection here")
}
if !q.id.eq(awp.a.id) {
panic("canary - version queue stack and selected project stack are misaligned")
}
// Advance the queue past the current version, which we know is bad
// TODO(sdboyer) is it feasible to make available the failure reason here?
if q.advance(nil) == nil && !q.isExhausted() {
// Search for another acceptable version of this failed dep in its queue
s.traceCheckQueue(q, awp.bmi(), true, 0)
if s.findValidVersion(q, awp.pl) == nil {
// Found one! Put it back on the selected queue and stop
// backtracking
// reusing the old awp is fine
awp.a.v = q.current()
s.selectAtom(awp, false)
break
}
}
s.traceBacktrack(awp.bmi(), false)
//s.traceInfo("no more versions of %s, backtracking", q.id.errString())
// No solution found; continue backtracking after popping the queue
// we just inspected off the list
// GC-friendly pop pointer elem in slice
s.vqs, s.vqs[len(s.vqs)-1] = s.vqs[:len(s.vqs)-1], nil
}
s.mtr.pop()
// Backtracking was successful if loop ended before running out of versions
if len(s.vqs) == 0 {
return false
}
s.attempts++
return true
}
func (s *solver) nextUnselected() (bimodalIdentifier, bool) {
if len(s.unsel.sl) > 0 {
return s.unsel.sl[0], true
}
return bimodalIdentifier{}, false
}