def guess_bad_solve(self, specs): # TODO: Check features as well from conda.console import setup_verbose_handlers setup_verbose_handlers() def mysat(specs): dists = self.get_dists(specs) groups = build_groups(dists) m, v, w = self.build_vw(groups) clauses = set(self.gen_clauses(v, groups, specs)) return sat(clauses) # Don't show the dots from solve2 in normal mode but do show the # dotlog messages with --debug dotlog.setLevel(logging.INFO) specs = [s for s in specs if not s.optional] hint = minimal_unsatisfiable_subset(specs, sat=mysat, log=True) if not hint: return '' hint = list(map(str, hint)) if len(hint) == 1: # TODO: Generate a hint from the dependencies. ret = (("\nHint: '{0}' has unsatisfiable dependencies (see 'conda " "info {0}')").format(hint[0].split()[0])) else: ret = """ Hint: the following packages conflict with each other: - %s Use 'conda info %s' etc. to see the dependencies for each package.""" % ('\n - '.join(hint), hint[0].split()[0]) return ret
def guess_bad_solve(self, specs, features): # TODO: Check features as well from conda.console import setup_verbose_handlers setup_verbose_handlers() # Don't show the dots from solve2 in normal mode but do show the # dotlog messages with --debug dotlog.setLevel(logging.WARN) def sat(specs): try: self.solve2(specs, features, guess=False, unsat_only=True) except RuntimeError: return False return True hint = minimal_unsatisfiable_subset(specs, sat=sat, log=True) if not hint: return '' if len(hint) == 1: # TODO: Generate a hint from the dependencies. return (( "\nHint: '{0}' has unsatisfiable dependencies (see 'conda " "info {0}')").format(hint[0].split()[0])) return (""" Hint: the following packages conflict with each other: - %s""" % '\n - '.join(hint))
def guess_bad_solve(self, specs, features): # TODO: Check features as well from conda.console import setup_verbose_handlers setup_verbose_handlers() # Don't show the dots from solve2 in normal mode but do show the # dotlog messages with --debug dotlog.setLevel(logging.WARN) def sat(specs): try: self.solve2(specs, features, guess=False, unsat_only=True) except RuntimeError: return False return True hint = minimal_unsatisfiable_subset(specs, sat=sat, log=True) if not hint: return '' if len(hint) == 1: # TODO: Generate a hint from the dependencies. return (("\nHint: '{0}' has unsatisfiable dependencies (see 'conda " "info {0}')").format(hint[0].split()[0])) return (""" Hint: the following packages conflict with each other: - %s""" % '\n - '.join(hint))
def minimal_unsatisfiable_subset(self, clauses, v, w): clauses = minimal_unsatisfiable_subset(clauses, log=True) pretty_clauses = [] for clause in clauses: if clause[0] < 0 and len(clause) > 1: pretty_clauses.append('%s => %s' % (self.clause_pkg_name(-clause[0], w), ' or '.join([self.clause_pkg_name(j, w) for j in clause[1:]]))) else: pretty_clauses.append(' or '.join([self.clause_pkg_name(j, w) for j in clause])) return "The following set of clauses is unsatisfiable:\n\n%s" % '\n'.join(pretty_clauses)
def test_minimal_unsatisfiable_subset(): assert raises(ValueError, lambda: minimal_unsatisfiable_subset([[1]])) clauses = [[-10], [1], [5], [2, 3], [3, 4], [5, 2], [-7], [2], [3], [-2, -3, 5], [7, 8, 9, 10], [-8], [-9]] res = minimal_unsatisfiable_subset(clauses) assert sorted(res) == [[-10], [-9], [-8], [-7], [7, 8, 9, 10]] assert not sat(res) clauses = [[1, 3], [2, 3], [-1], [4], [3], [-3]] for perm in permutations(clauses): res = minimal_unsatisfiable_subset(clauses) assert sorted(res) == [[-3], [3]] assert not sat(res) clauses = [[1], [-1], [2], [-2], [3, 4], [4]] for perm in permutations(clauses): res = minimal_unsatisfiable_subset(perm) assert sorted(res) in [[[-1], [1]], [[-2], [2]]] assert not sat(res)
def test_minimal_unsatisfiable_subset(): def sat(val): return Clauses(max(abs(v) for v in chain(*val))).sat(val) assert raises(ValueError, lambda: minimal_unsatisfiable_subset([[1]], sat)) clauses = [[-10], [1], [5], [2, 3], [3, 4], [5, 2], [-7], [2], [3], [-2, -3, 5], [7, 8, 9, 10], [-8], [-9]] res = minimal_unsatisfiable_subset(clauses, sat) assert sorted(res) == [[-10], [-9], [-8], [-7], [7, 8, 9, 10]] assert not sat(res) clauses = [[1, 3], [2, 3], [-1], [4], [3], [-3]] for perm in permutations(clauses): res = minimal_unsatisfiable_subset(clauses, sat) assert sorted(res) == [[-3], [3]] assert not sat(res) clauses = [[1], [-1], [2], [-2], [3, 4], [4]] for perm in permutations(clauses): res = minimal_unsatisfiable_subset(perm, sat) assert sorted(res) in [[[-1], [1]], [[-2], [2]]] assert not sat(res)
def get_dists(self, specs, sat_only=False): log.debug('Beginning the pruning process') specs = list(map(MatchSpec, specs)) active = self.feats.copy() len0 = len(specs) bad_deps = [] valid = {} unsat = [] def filter_group(matches, top): # If no packages exist with this name, it's a fatal error match1 = next(x for x in matches) name = match1.name group = self.groups.get(name,[]) if not group: bad_deps.append((matches,top)) return False # If we are here, then this dependency is mandatory, # so add it to the master list. That way it is still # participates in the pruning even if one of its # parents is pruned away if all(name != ms.name for ms in specs): specs.append(MatchSpec(name, parent=str(top))) # Prune packages that don't match any of the patterns # or which may be missing dependencies nold = nnew = 0 first = False notfound = set() for fn in group: sat = valid.get(fn, None) if sat is None: first = sat = valid[fn] = True nold += sat if sat: if name[-1] == '@': sat = name[:-1] in self.track_features(fn) else: sat = self.match_any(matches, fn) if sat: sat = all(any(valid.get(f2, True) for f2 in self.find_matches(ms)) for ms in self.ms_depends(fn) if not ms.optional) if not sat: notfound.update(ms for ms in self.ms_depends(fn) if ms.name not in self.groups) nnew += sat valid[fn] = sat reduced = nnew < nold if reduced: log.debug('%s: pruned from %d -> %d' % (name, nold, nnew)) if nnew == 0: if notfound: bad_deps.append((notfound,matches)) unsat.extend(matches) return True elif not first: return False # Perform the same filtering steps on any dependencies shared across # *all* packages in the group. Even if just one of the packages does # not have a particular dependency, it must be ignored in this pass. cdeps = defaultdict(list) for fn in group: if valid[fn]: for m2 in self.ms_depends(fn): cdeps[m2.name].append(m2) if top is None: top = match1 cdeps = {mname:set(deps) for mname,deps in iteritems(cdeps) if len(deps)==nnew} if cdeps: top = top if top else match1 if sum(filter_group(deps, top) for deps in itervalues(cdeps)): reduced = True return reduced # Look through all of the non-optional specs (which at this point # should include the installed packages) for any features which *might* # be installed. Prune away any packages that depend on features other # than this subset. def prune_features(): feats = set() for ms in specs: for fn in self.groups.get(ms.name, []): if valid.get(fn, True): feats.update(self.track_features(fn)) pruned = False for feat in active - feats: active.remove(feat) for fn in self.groups[feat+'@']: if valid.get(fn,True): valid[fn] = False pruned = True for name, group in iteritems(self.groups): nold = npruned = 0 for fn in group: if valid.get(fn, True): nold += 1 if self.features(fn) - feats: valid[fn] = False npruned += 1 if npruned: pruned = True log.debug('%s: pruned from %d -> %d for missing features'%(name,nold,nold-npruned)) if npruned == nold: for ms in specs: if ms.name == name and not ms.optional: bad_deps.append((ms,name+'@')) return pruned # Initial scan to add tracked features and rule out missing packages for feat in self.feats: valid[feat + '@'] = False for ms in specs: if ms.name[-1] == '@': feat = ms.name[:-1] self.add_feature(feat) valid[feat + '@'] = True elif not ms.optional: if not any(True for _ in self.find_matches(ms)): bad_deps.append(ms) if bad_deps: raise NoPackagesFound( "No packages found in current %s channels matching: %s" % (config.subdir, ' '.join(map(str,bad_deps))), bad_deps) # Iterate in the filtering process until no more progress is made pruned = True while pruned: pruned = False for s in list(specs): if not s.optional: pruned += filter_group([s], None) if unsat and sat_only: return False pruned += prune_features() log.debug('Potential feature set: %r'%(active,)) # Touch all packages touched = {} def is_valid(fn, notfound=None): val = valid.get(fn) if val is None or (notfound and not val): valid[fn] = True # ensure cycles terminate val = valid[fn] = all(any(is_valid(f2) for f2 in self.find_matches(ms)) for ms in self.ms_depends(fn)) if notfound and not val: notfound.append(ms) return val def touch(fn, notfound=None): val = touched.get(fn) if val is None or (notfound is not None and not val): val = touched[fn] = is_valid(fn, notfound) if val: for ms in self.ms_depends(fn): for f2 in self.find_matches(ms): touch(f2, notfound) return val for ms in specs[:len0]: notfound = [] if sum(touch(fn, notfound) for fn in self.find_matches(ms)) == 0 and not ms.optional and notfound: bad_deps.extend((notfound,(ms))) if bad_deps: res = [] specs = set() for spec, src in bad_deps: specs.update(spec) res.append(' - %s: %s' % ('|'.join(map(str,src)),', '.join(map(str,spec)))) raise NoPackagesFound('\n'.join([ "Could not find some dependencies for one or more packages:"] + res), specs) # Throw an error for missing packages or dependencies if sat_only and (unsat or bad_deps): return False # For weak dependency conflicts, generate a hint if unsat: def mysat(specs): self.get_dists(specs, sat_only=True) stderrlog.info('\nError: Unsatisfiable package specifications.\nGenerating hint: \n') hint = minimal_unsatisfiable_subset(specs, sat=mysat, log=True) hint = [' - %s'%str(x) for x in set(chain(unsat, hint))] hint = (['The following specifications were found to be in conflict:'] + hint + ['Use "conda info <package>" to see the dependencies for each package.']) sys.exit('\n'.join(hint)) dists = {fn:info for fn,info in iteritems(self.index) if touched.get(fn)} return dists, specs
def get_dists(self, specs): log.debug('Retrieving packages for: %s' % specs) specs, optional, features = self.verify_specs(specs) filter = {} touched = {} snames = set() nspecs = set() unsat = set() def filter_group(matches, chains=None): # If we are here, then this dependency is mandatory, # so add it to the master list. That way it is still # participates in the pruning even if one of its # parents is pruned away if unsat: return False match1 = next(ms for ms in matches) name = match1.name first = name not in snames group = self.groups.get(name, []) # Prune packages that don't match any of the patterns # or which have unsatisfiable dependencies nold = 0 bad_deps = [] for fkey in group: if filter.setdefault(fkey, True): nold += 1 sat = self.match_any(matches, fkey) sat = sat and all(any(filter.get(f2, True) for f2 in self.find_matches(ms)) for ms in self.ms_depends(fkey)) filter[fkey] = sat if not sat: bad_deps.append(fkey) # Build dependency chains if we detect unsatisfiability nnew = nold - len(bad_deps) reduced = nnew < nold if reduced: log.debug('%s: pruned from %d -> %d' % (name, nold, nnew)) if nnew == 0: if name in snames: snames.remove(name) bad_deps = [fkey for fkey in bad_deps if self.match_any(matches, fkey)] matches = [(ms,) for ms in matches] chains = [a + b for a in chains for b in matches] if chains else matches if bad_deps: dep2 = set() for fkey in bad_deps: for ms in self.ms_depends(fkey): if not any(filter.get(f2, True) for f2 in self.find_matches(ms)): dep2.add(ms) chains = [a + (b,) for a in chains for b in dep2] unsat.update(chains) return nnew != 0 if not reduced and not first: return False # Perform the same filtering steps on any dependencies shared across # *all* packages in the group. Even if just one of the packages does # not have a particular dependency, it must be ignored in this pass. if first: snames.add(name) if match1 not in specs: nspecs.add(MatchSpec(name)) cdeps = defaultdict(list) for fkey in group: if filter[fkey]: for m2 in self.ms_depends(fkey): if m2.name[0] != '@' and not m2.optional: cdeps[m2.name].append(m2) cdeps = {mname: set(deps) for mname, deps in iteritems(cdeps) if len(deps) >= nnew} if cdeps: matches = [(ms,) for ms in matches] if chains: matches = [a + b for a in chains for b in matches] if sum(filter_group(deps, chains) for deps in itervalues(cdeps)): reduced = True return reduced # Iterate in the filtering process until no more progress is made def full_prune(specs, optional, features): self.default_filter(features, filter) for ms in optional: for fkey in self.groups.get(ms.name, []): if not self.match_fast(ms, fkey): filter[fkey] = False feats = set(self.trackers.keys()) snames.clear() specs = slist = list(specs) onames = set(s.name for s in specs) for iter in range(10): first = True while sum(filter_group([s]) for s in slist) and not unsat: slist = specs + [MatchSpec(n) for n in snames - onames] first = False if unsat: return False if first and iter: return True touched.clear() for fstr in features: touched[fstr+'@'] = True for spec in chain(specs, optional): self.touch(spec, touched, filter) nfeats = set() for fkey, val in iteritems(touched): if val: nfeats.update(self.track_features(fkey)) if len(nfeats) >= len(feats): return True pruned = False for feat in feats - nfeats: feats.remove(feat) for fkey in self.trackers[feat]: if filter.get(fkey, True): filter[fkey] = False pruned = True if not pruned: return True # # In the case of a conflict, look for the minimum satisfiable subset # if not full_prune(specs, optional, features): def minsat_prune(specs): return full_prune(specs, optional, features) save_unsat = set(s for s in unsat if s[0] in specs) stderrlog.info('...') hint = minimal_unsatisfiable_subset(specs, sat=minsat_prune, log=False) save_unsat.update((ms,) for ms in hint) raise Unsatisfiable(save_unsat) dists = {fkey: self.index[fkey] for fkey, val in iteritems(touched) if val} return dists, list(map(MatchSpec, snames - {ms.name for ms in specs}))
def solve(self, specs, len0=None, returnall=False): try: stdoutlog.info("Solving package specifications ...") dotlog.debug("Solving for %s" % (specs,)) # Find the compliant packages specs = list(map(MatchSpec, specs)) if len0 is None: len0 = len(specs) dists, new_specs, unsat = self.get_dists(specs, True) if not dists and not unsat: return False if dists is None else ([[]] if returnall else []) # Check if satisfiable dotlog.debug('Checking satisfiability') r2 = Resolve(dists, True, True) C = r2.gen_clauses() constraints = r2.generate_spec_constraints(C, specs) solution = C.sat(constraints, True) if not solution: # Generate hint. First we determine a minimum unsat subset. This is the # smallest set of specs that conflict with each other def mysat(specs): constraints = r2.generate_spec_constraints(C, specs) res = C.sat(constraints, False) is not None return res stdoutlog.info("\nUnsatisfiable specifications detected; generating hint ...") hint = minimal_unsatisfiable_subset(specs, sat=mysat, log=False) # Find dependency chains that establish invalidity hnames = set(h.name for h in hint) filter = {} for ms2 in hint: for fkey in r2.find_matches(ms2.name): if not self.match_fast(ms2, fkey): filter[fkey] = False if unsat: hnames.add(unsat) for fkey in r2.find_matches(unsat): filter[fkey] = False chains = [] for ms in hint: # This ensures the chains see the same filter; produces more hints res = self.invalid_chains(ms, filter.copy()) # Only consider chains that end in one of the packages we care about res = [r for r in res if str(r[-1]).split(' ', 1)[0] in hnames] chains.extend(res or [(ms.spec,)]) raise Unsatisfiable(chains) speco = [] # optional packages specr = [] # requested packages speca = [] # all other packages specm = set(r2.groups) # missing from specs for k, s in enumerate(chain(specs, new_specs)): if s.name in specm: specm.remove(s.name) if not s.optional: (specr if k < len0 else speca).append(s) elif any(r2.find_matches(s)): s = MatchSpec(s.name, optional=True, target=s.target) speco.append(s) speca.append(s) speca.extend(MatchSpec(s) for s in specm) # Removed packages: minimize count eq_optional_c = r2.generate_removal_count(C, speco) solution, obj7 = C.minimize(eq_optional_c, solution) dotlog.debug('Package removal metric: %d' % obj7) # Requested packages: maximize versions, then builds eq_req_v, eq_req_b = r2.generate_version_metrics(C, specr) solution, obj3 = C.minimize(eq_req_v, solution) solution, obj4 = C.minimize(eq_req_b, solution) dotlog.debug('Initial package version/build metrics: %d/%d' % (obj3, obj4)) # Track features: minimize feature count eq_feature_count = r2.generate_feature_count(C) solution, obj1 = C.minimize(eq_feature_count, solution) dotlog.debug('Track feature count: %d' % obj1) # Featured packages: maximize featured package count eq_feature_metric, ftotal = r2.generate_feature_metric(C) solution, obj2 = C.minimize(eq_feature_metric, solution) obj2 = ftotal - obj2 dotlog.debug('Package feature count: %d' % obj2) # Remaining packages: maximize versions, then builds, then count eq_v, eq_b = r2.generate_version_metrics(C, speca) solution, obj5 = C.minimize(eq_v, solution) solution, obj6 = C.minimize(eq_b, solution) dotlog.debug('Additional package version/build metrics: %d/%d' % (obj5, obj6)) # Prune unnecessary packages eq_c = r2.generate_package_count(C, specm) solution, obj7 = C.minimize(eq_c, solution, trymax=True) dotlog.debug('Weak dependency count: %d' % obj7) def clean(sol): return [q for q in (C.from_index(s) for s in sol) if q and q[0] != '!' and '@' not in q] dotlog.debug('Looking for alternate solutions') nsol = 1 psolutions = [] psolution = clean(solution) psolutions.append(psolution) while True: nclause = tuple(C.Not(C.from_name(q)) for q in psolution) solution = C.sat((nclause,), True) if solution is None: break nsol += 1 if nsol > 10: dotlog.debug('Too many solutions; terminating') break psolution = clean(solution) psolutions.append(psolution) if nsol > 1: psols2 = list(map(set, psolutions)) common = set.intersection(*psols2) diffs = [sorted(set(sol) - common) for sol in psols2] stdoutlog.info( '\nWarning: %s possible package resolutions ' '(only showing differing packages):%s%s' % ('>10' if nsol > 10 else nsol, dashlist(', '.join(diff) for diff in diffs), '\n ... and others' if nsol > 10 else '')) def stripfeat(sol): return sol.split('[')[0] stdoutlog.info('\n') if returnall: return [sorted(map(stripfeat, psol)) for psol in psolutions] else: return sorted(map(stripfeat, psolutions[0])) except: stdoutlog.info('\n') raise