diff --git a/internal/gps/satisfy.go b/internal/gps/satisfy.go index 9638d766ff..c7031e65e7 100644 --- a/internal/gps/satisfy.go +++ b/internal/gps/satisfy.go @@ -54,6 +54,9 @@ func (s *solver) check(a atomWithPackages, pkgonly bool) error { if err = s.checkIdentMatches(a, dep); err != nil { return err } + if err = s.checkCaseConflicts(a, dep); err != nil { + return err + } if err = s.checkDepsConstraintsAllowable(a, dep); err != nil { return err } @@ -218,6 +221,31 @@ func (s *solver) checkIdentMatches(a atomWithPackages, cdep completeDep) error { return nil } +// checkCaseConflicts ensures that the ProjectRoot specified in the completeDep +// does not have case conflicts with any existing dependencies. +// +// We only need to check the ProjectRoot, rather than any packages therein, as +// the later check for package existence is case-sensitive. +func (s *solver) checkCaseConflicts(a atomWithPackages, cdep completeDep) error { + pr := cdep.workingConstraint.Ident.ProjectRoot + hasConflict, current := s.sel.findCaseConflicts(pr) + if !hasConflict { + return nil + } + + curid, _ := s.sel.getIdentFor(pr) + deps := s.sel.getDependenciesOn(curid) + for _, d := range deps { + s.fail(d.depender.id) + } + + return &caseMismatchFailure{ + goal: dependency{depender: a.a, dep: cdep}, + current: current, + failsib: deps, + } +} + // checkPackageImportsFromDepExist ensures that, if the dep is already selected, // the newly-required set of packages being placed on it exist and are valid. func (s *solver) checkPackageImportsFromDepExist(a atomWithPackages, cdep completeDep) error { diff --git a/internal/gps/selection.go b/internal/gps/selection.go index e29d83fe53..02ea1f2d5b 100644 --- a/internal/gps/selection.go +++ b/internal/gps/selection.go @@ -4,9 +4,12 @@ package gps +import "strings" + type selection struct { projects []selected deps map[ProjectRoot][]dependency + prLenMap map[int][]ProjectRoot vu *versionUnifier } @@ -59,13 +62,43 @@ func (s *selection) popSelection() (atomWithPackages, bool) { return sel.a, sel.first } +func (s *selection) findCaseConflicts(pr ProjectRoot) (bool, ProjectRoot) { + prlist, has := s.prLenMap[len(pr)] + if !has { + return false, "" + } + + // TODO(sdboyer) bug here if it's possible that strings.ToLower() could + // change the length of the string + lowpr := strings.ToLower(string(pr)) + for _, existing := range prlist { + if lowpr != strings.ToLower(string(existing)) { + continue + } + // If the converted strings match, then whatever we figure out here will + // be definitive - we needn't walk the rest of the slice. + if pr == existing { + return false, "" + } else { + return true, existing + } + } + + return false, "" +} + func (s *selection) pushDep(dep dependency) { - s.deps[dep.dep.Ident.ProjectRoot] = append(s.deps[dep.dep.Ident.ProjectRoot], dep) + pr := dep.dep.Ident.ProjectRoot + s.deps[pr] = append(s.deps[pr], dep) + s.prLenMap[len(pr)] = append(s.prLenMap[len(pr)], pr) } func (s *selection) popDep(id ProjectIdentifier) (dep dependency) { deps := s.deps[id.ProjectRoot] dep, s.deps[id.ProjectRoot] = deps[len(deps)-1], deps[:len(deps)-1] + + prlist := s.prLenMap[len(id.ProjectRoot)] + s.prLenMap[len(id.ProjectRoot)] = prlist[:len(prlist)-1] return dep } diff --git a/internal/gps/solve_failures.go b/internal/gps/solve_failures.go index e6a2c47a85..b3779756cc 100644 --- a/internal/gps/solve_failures.go +++ b/internal/gps/solve_failures.go @@ -71,6 +71,49 @@ func (e *noVersionError) traceString() string { return buf.String() } +// caseMismatcFailure occurs when there are import paths that differ only by +// case. The compiler disallows this case. +type caseMismatchFailure struct { + // goal is the depender atom that tried to introduce the case-varying name, + // along with the case-varying name. + goal dependency + // current is the specific casing of a ProjectRoot that is presently + // selected for all possible case variations of its contained unicode code + // points. + current ProjectRoot + // failsib is the list of active dependencies that have determined the + // specific casing for the target project. + failsib []dependency +} + +func (e *caseMismatchFailure) Error() string { + if len(e.failsib) == 1 { + str := "Could not introduce %s due to a case-only variation: it depends on %q, but %q was already established as the case variant for that project root by depender %s" + return fmt.Sprintf(str, a2vs(e.goal.depender), e.goal.dep.Ident.ProjectRoot, e.current, a2vs(e.failsib[0].depender)) + } + + var buf bytes.Buffer + + str := "Could not introduce %s due to a case-only variation: it depends on %q, but %q was already established as the case variant for that project root by the following other dependers:\n" + fmt.Fprintf(&buf, str, e.goal.dep.Ident.ProjectRoot, e.current, a2vs(e.goal.depender)) + + for _, c := range e.failsib { + fmt.Fprintf(&buf, "\t%s\n", a2vs(c.depender)) + } + + return buf.String() +} + +func (e *caseMismatchFailure) traceString() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "case-only variation in dependency on %q; %q already established by:\n", e.goal.dep.Ident.ProjectRoot, e.current) + for _, f := range e.failsib { + fmt.Fprintf(&buf, "%s\n", a2vs(f.depender)) + } + + return buf.String() +} + // disjointConstraintFailure occurs when attempting to introduce an atom that // itself has an acceptable version, but one of its dependency constraints is // disjoint with one or more dependency constraints already active for that diff --git a/internal/gps/solver.go b/internal/gps/solver.go index 0515f199dd..9e91665b29 100644 --- a/internal/gps/solver.go +++ b/internal/gps/solver.go @@ -307,8 +307,9 @@ func Prepare(params SolveParameters, sm SourceManager) (Solver, error) { // Initialize stacks and queues s.sel = &selection{ - deps: make(map[ProjectRoot][]dependency), - vu: s.vUnify, + deps: make(map[ProjectRoot][]dependency), + prLenMap: make(map[int][]ProjectRoot), + vu: s.vUnify, } s.unsel = &unselected{ sl: make([]bimodalIdentifier, 0),