/* * solver3.cc - The APT 3.0 solver * * Copyright (c) 2023 Julian Andres Klode * Copyright (c) 2023 Canonical Ltd * * SPDX-License-Identifier: GPL-2.0+ * * This solver started from scratch but turns slowly into a variant of * MiniSat as documented in the paper * "An extensible SAT-solver [extended version 1.2]." * by Niklas Eén, and Niklas Sörensson. * * It extends MiniSAT with support for optional clauses, and differs * in that it removes non-deterministic aspects like the activity based * ordering. Instead it uses a more nuanced static ordering that, to * some extend, preserves some greediness and sub-optimality of the * classic APT solver. */ #define APT_COMPILING_APT #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // FIXME: Helpers stolen from DepCache, please give them back. struct APT::Solver::CompareProviders3 /*{{{*/ { pkgCache &Cache; pkgDepCache::Policy &Policy; pkgCache::PkgIterator const Pkg; APT::Solver &Solver; pkgCache::VerIterator bestVersion(pkgCache::PkgIterator pkg) { pkgCache::VerIterator res = pkg.VersionList(); for (auto v = res; not v.end(); ++v) res = std::max(res, v, *this); return res; } bool operator()(Var a, Var b) { pkgCache::VerIterator va = a.Ver(Cache); pkgCache::VerIterator vb = b.Ver(Cache); if (auto pa = a.Pkg(Cache)) va = bestVersion(pa); if (auto pb = b.Pkg(Cache)) vb = bestVersion(pb); assert(not va.end() && not vb.end()); return (*this)(va, vb); } bool operator()(pkgCache::VerIterator const &AV, pkgCache::VerIterator const &BV) { assert(not AV.end() && not BV.end()); pkgCache::PkgIterator const A = AV.ParentPkg(); pkgCache::PkgIterator const B = BV.ParentPkg(); // Compare versions for the same package. FIXME: Move this to the real implementation if (A == B) { if (AV == BV) return false; // Candidate wins in upgrade scenario if (Solver.IsUpgrade) { auto Cand = Solver.GetCandidateVer(A); if (AV == Cand || BV == Cand) return (AV == Cand); } // Installed version wins otherwise if (A.CurrentVer() == AV || B.CurrentVer() == BV) return (A.CurrentVer() == AV); // Rest is ordered list, first by priority if (auto pinA = Solver.GetPriority(AV), pinB = Solver.GetPriority(BV); pinA != pinB) return pinA > pinB; // Then by version return _system->VS->CmpVersion(AV.VerStr(), BV.VerStr()) > 0; } // Try obsolete choices only after exhausting non-obsolete choices such that we install // packages replacing them and don't keep back upgrades depending on the replacement to // keep the obsolete package installed. if (Solver.IsUpgrade) if (auto obsoleteA = Solver.Obsolete(A), obsoleteB = Solver.Obsolete(B); obsoleteA != obsoleteB) return obsoleteB; // Prefer MA:same packages if other architectures for it are installed if ((AV->MultiArch & pkgCache::Version::Same) == pkgCache::Version::Same || (BV->MultiArch & pkgCache::Version::Same) == pkgCache::Version::Same) { bool instA = false; if ((AV->MultiArch & pkgCache::Version::Same) == pkgCache::Version::Same) { pkgCache::GrpIterator Grp = A.Group(); for (pkgCache::PkgIterator P = Grp.PackageList(); P.end() == false; P = Grp.NextPkg(P)) if (P->CurrentVer != 0) { instA = true; break; } } bool instB = false; if ((BV->MultiArch & pkgCache::Version::Same) == pkgCache::Version::Same) { pkgCache::GrpIterator Grp = B.Group(); for (pkgCache::PkgIterator P = Grp.PackageList(); P.end() == false; P = Grp.NextPkg(P)) { if (P->CurrentVer != 0) { instB = true; break; } } } if (instA != instB) return instA; } if ((A->CurrentVer == 0 || B->CurrentVer == 0) && A->CurrentVer != B->CurrentVer) return A->CurrentVer != 0; // Prefer packages in the same group as the target; e.g. foo:i386, foo:amd64 if (A->Group != B->Group) { if (A->Group == Pkg->Group && B->Group != Pkg->Group) return true; else if (B->Group == Pkg->Group && A->Group != Pkg->Group) return false; } // we like essentials if ((A->Flags & pkgCache::Flag::Essential) != (B->Flags & pkgCache::Flag::Essential)) { if ((A->Flags & pkgCache::Flag::Essential) == pkgCache::Flag::Essential) return true; else if ((B->Flags & pkgCache::Flag::Essential) == pkgCache::Flag::Essential) return false; } if ((A->Flags & pkgCache::Flag::Important) != (B->Flags & pkgCache::Flag::Important)) { if ((A->Flags & pkgCache::Flag::Important) == pkgCache::Flag::Important) return true; else if ((B->Flags & pkgCache::Flag::Important) == pkgCache::Flag::Important) return false; } // prefer native architecture if (strcmp(A.Arch(), B.Arch()) != 0) { if (strcmp(A.Arch(), A.Cache()->NativeArch()) == 0) return true; else if (strcmp(B.Arch(), B.Cache()->NativeArch()) == 0) return false; std::vector archs = APT::Configuration::getArchitectures(); for (std::vector::const_iterator a = archs.begin(); a != archs.end(); ++a) if (*a == A.Arch()) return true; else if (*a == B.Arch()) return false; } // higher priority seems like a good idea if (AV->Priority != BV->Priority) return AV->Priority < BV->Priority; if (auto NameCmp = strcmp(A.Name(), B.Name())) return NameCmp < 0; // unable to decide… return A->ID > B->ID; } }; /** \brief Returns \b true for packages matching a regular * expression in APT::NeverAutoRemove. */ class DefaultRootSetFunc2 : public pkgDepCache::DefaultRootSetFunc { std::unique_ptr Kernels; public: DefaultRootSetFunc2(pkgCache *cache) : Kernels(APT::KernelAutoRemoveHelper::GetProtectedKernelsFilter(cache)) {}; ~DefaultRootSetFunc2() override = default; bool InRootSet(const pkgCache::PkgIterator &pkg) override { return pkg.end() == false && ((*Kernels)(pkg) || DefaultRootSetFunc::InRootSet(pkg)); }; }; // FIXME: DEDUP with pkgDepCache. /*}}}*/ APT::Solver::Solver(pkgCache &cache, pkgDepCache::Policy &policy, EDSP::Request::Flags requestFlags) : cache(cache), policy(policy), rootState(new State), pkgStates(cache), verStates(cache), pkgObsolete(cache), priorities(cache), candidates(cache), requestFlags(requestFlags) { // Ensure trivially static_assert(std::is_trivially_destructible_v); static_assert(std::is_trivially_destructible_v); static_assert(sizeof(APT::Solver::Var) == sizeof(map_pointer)); static_assert(sizeof(APT::Solver::Var) == sizeof(map_pointer)); // Root state is "true". rootState->decision = Decision::MUST; } // This function determines if a work item is less important than another. bool APT::Solver::Work::operator<(APT::Solver::Work const &b) const { if ((not clause->optional && size < 2) != (not b.clause->optional && b.size < 2)) return not b.clause->optional && b.size < 2; if (clause->optional != b.clause->optional) return clause->optional; if (clause->group != b.clause->group) return clause->group > b.clause->group; if ((size < 2) != (b.size < 2)) return b.size < 2; if (size == 1 && b.size == 1) // Special case: 'shortcircuit' optional packages return clause->solutions.size() < b.clause->solutions.size(); return false; } std::string APT::Solver::Clause::toString(pkgCache &cache, bool pretty) const { std::string out; out.append(reason.toString(cache)); if (dep && pretty) { out.append(" ").append(pkgCache::DepIterator(cache, dep).DepType()).append(" "); for (auto dep = pkgCache::DepIterator(cache, this->dep); not dep.end(); ++dep) { out.append(dep.TargetPkg().FullName(true)); if (dep.TargetVer()) out.append(" (").append(dep.CompType()).append(" ").append(dep.TargetVer()).append(")"); if (!(dep->CompareOp & pkgCache::Dep::Or)) break; out.append(" | "); } } else if (pretty && group == Group::SelectVersion && negative) out.append(" conflicts with other versions of itself"); else if (pretty && group == Group::SelectVersion && reason.Pkg()) { out.append(solutions.size() > 1 ? " is available in versions " : " is available in version "); for (auto var : solutions) { assert(var.Ver()); if (var != solutions.front()) out.append(", "); out.append(var.Ver(cache).VerStr()); } } else { out.append(" -> "); for (auto var : solutions) out.append(" | ").append(var.toString(cache)); } return out; } std::string APT::Solver::Work::toString(pkgCache &cache) const { std::ostringstream out; if (erased) out << "Erased "; if (clause->optional) out << "Optional "; out << "Item (" << ssize_t(size <= clause->solutions.size() ? size : -1) << "@" << depth << ") "; out << clause->toString(cache); return out.str(); } inline APT::Solver::Var APT::Solver::bestReason(APT::Solver::Clause const *clause, APT::Solver::Var var) const { if (not clause) return Var{}; if (clause->reason == var) for (auto choice : clause->solutions) { if (clause->negative && (*this)[choice].decision == Decision::MUST) return choice; if (not clause->negative && (*this)[choice].decision == Decision::MUSTNOT) return choice; } return clause->reason; } // Prints an implication graph part of the form A -> B -> C, possibly with "not" std::string APT::Solver::WhyStr(Var reason) const { std::vector out; while (not reason.empty()) { if ((*this)[reason].decision == Decision::MUSTNOT) out.push_back(std::string("not ") + reason.toString(cache)); else out.push_back(reason.toString(cache)); reason = bestReason((*this)[reason].reason, reason); } std::string outstr; for (auto I = out.rbegin(); I != out.rend(); ++I) { outstr += (outstr.size() == 0 ? "" : " -> ") + *I; } return outstr; } std::string APT::Solver::LongWhyStr(Var var, bool decision, const Clause *rclause, std::string prefix, std::unordered_set &seen) const { std::ostringstream out; // Helper function to nicely print more details than just "install/do not install", such as "removal", "upgrade", "downgrade", "install" auto printSelection = [this](Var var, bool decision) { std::string s; if (auto pkg = var.Pkg(cache); not decision && pkg && pkg->CurrentVer) strprintf(s, "%s is selected for removal", var.toString(cache).c_str()); else if (auto ver = var.Ver(cache); decision && ver && ver.ParentPkg().CurrentVer() && ver.ParentPkg().CurrentVer() != ver) { if (cache.VS->CmpVersion(ver.ParentPkg().CurrentVer().VerStr(), ver.VerStr()) < 0) strprintf(s, "%s is selected as an upgrade", var.toString(cache).c_str()); else strprintf(s, "%s is selected as a downgrade", var.toString(cache).c_str()); } else if (not decision) strprintf(s, "%s is not selected for install", var.toString(cache).c_str()); else strprintf(s, "%s is selected for install", var.toString(cache).c_str()); return s; }; // Helper: Recurse into all of the children of the clause and print the decision for them. auto recurseChildren = [&](const Clause *clause, Var skip = Var()) { if (clause->solutions.empty()) out << prefix << "[no choices]\n"; for (auto choice : clause->solutions) { if (choice == skip) continue; if ((*this)[choice].decision == Decision::NONE) out << prefix << "- " << choice.toString(cache) << " is undecided\n"; else out << prefix << "- " << LongWhyStr(choice, (*this)[choice].decision == Decision::MUST, (*this)[choice].reason, prefix + " ", seen).substr(prefix.size() + 2); } }; // Inverse version selection clauses that select the package if the version is selected, // such as pkg=ver -> pkg, are irrelevant for the user, skip them if (var.Pkg() && decision && rclause && rclause->group == Group::SelectVersion) { var = rclause->reason; rclause = (*this)[var].reason; } // No reason given, probably a user request or manually installed or essential or whatnot. if (not rclause) { out << prefix << printSelection(var, decision) << "\n"; return out.str(); } // We could be called with a decision we tried to make but failed due to a conflict; // this checks if it is the real decision. if ((*this)[var].decision != Decision::NONE && decision == ((*this)[var].decision == Decision::MUST) && (*this)[var].reason == rclause) { // If we have seen the real decision before; we dont't need to print it again. if (seen.find(var) != seen.end()) { out << prefix << printSelection(var, decision) << " as above\n"; return out.str(); } seen.insert(var); } // A package was decided "not install" due to a positive clause, so the clause is unsat. if (not decision && rclause && not rclause->negative) { out << prefix << rclause->toString(cache, true) << "\n"; out << prefix << "but none of the choices are installable:\n"; recurseChildren(rclause); return out.str(); } // Build the strongest path from a root to our decision leaf std::vector path; for (auto reason = bestReason(rclause, var); not reason.empty(); reason = bestReason((*this)[reason].reason, reason)) path.push_back(reason); // Render the strong reasoning path out << prefix << printSelection(var, decision) << " because:\n"; auto w = std::to_string(path.size() + 1).size(); size_t i = 1; for (auto it = path.rbegin(); it != path.rend(); ++it, ++i) { auto const &state = (*this)[*it]; // Don't print version selection clauses if (it->Pkg() && state.reason && state.reason->group == Group::SelectVersion) { --i; continue; } if (seen.find(*it) != seen.end()) { if ((it + 1) == path.rend() || seen.find(*(it + 1)) == seen.end()) { out << prefix << (i == 1 ? "" : "1-") << i << ". " << printSelection(*it, state.decision == Decision::MUST) << " as above\n"; } continue; } seen.insert(*it); if (state.reason) { out << prefix << std::setw(w) << i << ". " << state.reason->toString(cache, true) << "\n"; if (state.reason->solutions.size() > 1) out << prefix << std::setw(w) << " " << " [selected " << it->toString(cache) << " for " << (state.decision == Decision::MUST ? "install" : "remove") << "]\n"; } else out << prefix << std::setw(w) << i << ". " << printSelection(*it, state.decision == Decision::MUST) << "\n"; } // Print the leaf. We can't have the leaf in the path because we might be called for an attempted decision // that conflicts with the actual assignment (to simplify: we marked X for not install, then we process Y depends X // and try to mark X and reach the conflict, we are called with "X" and the "Y depends X" clause). out << prefix << std::setw(w) << i << ". " << rclause->toString(cache, true) << "\n"; if (rclause->solutions.size() > 1) out << prefix << std::setw(w) << " " << " " << "[selected " << bestReason(rclause, var).toString(cache) << "]\n"; bool firstContext = true; // Show alternative paths not taken. Consider you have Y depends X|Z; and you mark X // for it; we show above Y -> X as the path, but here we go show why Z was not considered in "Y depends X|Z". for (auto it = path.rbegin(); it != path.rend(); ++it) { auto const &state = (*this)[*it]; if (not state.reason) // If we have no reason, we don't have alternatives continue; if (state.reason->solutions.size() <= 1) // Nothing to print if we no alternatives continue; if (state.reason->negative) // We only actually need one conflicting choice, ignore others continue; if (firstContext) { out << prefix << "For context, additional choices that could not be installed:" << "\n"; } firstContext = false; out << prefix << "* In " << state.reason->toString(cache, true) << ":\n"; prefix += " "; recurseChildren(state.reason, *it); prefix.resize(prefix.size() - 2); } if (rclause->solutions.size() > 1 && not rclause->negative) { if (firstContext) { out << prefix << "For context, additional choices that could not be installed:" << "\n"; } firstContext = false; out << prefix << "* In " << rclause->toString(cache, true) << ":\n"; prefix += " "; recurseChildren(rclause, var); prefix.resize(prefix.size() - 2); } return out.str(); } // This is essentially asking whether any other binary in the source package has a higher candidate // version. This pretends that each package is installed at the same source version as the package // under consideration. bool APT::Solver::ObsoletedByNewerSourceVersion(pkgCache::VerIterator cand) const { const auto pkg = cand.ParentPkg(); const int candPriority = GetPriority(cand); for (auto ver = cand.Cache()->FindGrp(cand.SourcePkgName()).VersionsInSource(); not ver.end(); ver = ver.NextInSource()) { // We are only interested in other packages in the same source package; built for the same architecture. if (ver->ParentPkg == cand->ParentPkg || ver.ParentPkg()->Arch != cand.ParentPkg()->Arch || cache.VS->CmpVersion(ver.SourceVerStr(), cand.SourceVerStr()) <= 0) continue; // We also take equal priority here, given that we have a higher version const int priority = GetPriority(ver); if (priority == 0 || priority < candPriority) continue; pkgObsolete[pkg] = 2; if (debug >= 3) std::cerr << "Obsolete: " << cand.ParentPkg().FullName() << "=" << cand.VerStr() << " due to " << ver.ParentPkg().FullName() << "=" << ver.VerStr() << "\n"; return true; } return false; } bool APT::Solver::Obsolete(pkgCache::PkgIterator pkg, bool AllowManual) const { if ((*this)[pkg].flags.manual && not AllowManual) return false; if (pkgObsolete[pkg] != 0) return pkgObsolete[pkg] == 2; auto ver = GetCandidateVer(pkg); if (ver.end() && not StrictPinning) ver = pkg.VersionList(); if (ver.end()) { if (debug >= 3) std::cerr << "Obsolete: " << pkg.FullName() << " - not installable\n"; pkgObsolete[pkg] = 2; return true; } if (ObsoletedByNewerSourceVersion(ver)) return true; if (ver.Downloadable()) { pkgObsolete[pkg] = 1; return false; } if (debug >= 3) std::cerr << "Obsolete: " << ver.ParentPkg().FullName() << "=" << ver.VerStr() << " - not installable\n"; pkgObsolete[pkg] = 2; return true; } bool APT::Solver::Assume(Var var, bool decision, const Clause *reason) { choices.push_back(solved.size()); return Enqueue(var, decision, std::move(reason)); } bool APT::Solver::Enqueue(Var var, bool decision, const Clause *reason) { auto &state = (*this)[var]; auto decisionCast = decision ? Decision::MUST : Decision::MUSTNOT; if (state.decision != Decision::NONE) { if (state.decision != decisionCast) { std::ostringstream err; err << "Unable to satisfy dependencies. Reached two conflicting decisions:" << "\n"; std::unordered_set seen; err << "1. " << LongWhyStr(var, state.decision == Decision::MUST, state.reason, " ", seen).substr(3) << "\n"; err << "2. " << LongWhyStr(var, decision, reason, " ", seen).substr(3); return _error->Error("%s", err.str().c_str()); } return true; } state.decision = decisionCast; state.depth = depth(); state.reason = reason; if (unlikely(debug >= 1)) std::cerr << "[" << depth() << "] " << (decision ? "Install" : "Reject") << ":" << var.toString(cache) << " (" << WhyStr(bestReason(reason, var)) << ")\n"; solved.push_back(Solved{var, std::nullopt}); propQ.push(var); return true; } bool APT::Solver::Propagate() { while (!propQ.empty()) { Var var = propQ.front(); propQ.pop(); if ((*this)[var].decision == Decision::MUST) { Discover(var); for (auto &clause : (*this)[var].clauses) if (not AddWork(Work{clause.get(), depth()})) return false; for (auto rclause : (*this)[var].rclauses) { if (not rclause->negative || rclause->optional || rclause->reason.empty()) continue; if (unlikely(debug >= 3)) std::cerr << "Propagate " << var.toString(cache) << " to NOT " << rclause->reason.toString(cache) << " for dep " << const_cast(rclause)->toString(cache) << std::endl; if (not Enqueue(rclause->reason, false, rclause)) return false; } } else if ((*this)[var].decision == Decision::MUSTNOT) { for (auto rclause : (*this)[var].rclauses) { if (rclause->negative || rclause->reason.empty()) continue; if ((*this)[rclause->reason].decision == Decision::MUSTNOT) continue; auto count = std::count_if(rclause->solutions.begin(), rclause->solutions.end(), [this](auto var) { return (*this)[var].decision != Decision::MUSTNOT; }); if (count == 1 && (*this)[rclause->reason].decision == Decision::MUST) { if (unlikely(debug >= 3)) std::cerr << "Propagate NOT " << var.toString(cache) << " to unit clause " << rclause->toString(cache); if (rclause->optional) { // Enqueue duplicated item, this will ensure we see it at the correct time if (not AddWork(Work{rclause, depth()})) return false; } else { // Find the variable that must be chosen and enqueue it as a fact for (auto sol : rclause->solutions) if ((*this)[sol].decision == Decision::NONE && not Enqueue(sol, true, rclause)) return false; } continue; } if (count >= 1 || rclause->optional) continue; if (unlikely(debug >= 3)) std::cerr << "Propagate NOT " << var.toString(cache) << " to " << rclause->reason.toString(cache) << " for dep " << const_cast(rclause)->toString(cache) << std::endl; if (not Enqueue(rclause->reason, false, rclause)) // Last version invalidated return false; } } } return true; } static bool SameOrGroup(pkgCache::DepIterator a, pkgCache::DepIterator b) { while (1) { if (a->DependencyData != b->DependencyData) return false; // At least one has reached the end, break if (not(a->CompareOp & pkgCache::Dep::Or) || not(b->CompareOp & pkgCache::Dep::Or)) break; ++a, ++b; } // Fully iterated over a and b return not(a->CompareOp & pkgCache::Dep::Or) && not(b->CompareOp & pkgCache::Dep::Or); } const APT::Solver::Clause *APT::Solver::RegisterClause(Clause &&clause) { auto &clauses = (*this)[clause.reason].clauses; clauses.push_back(std::make_unique(std::move(clause))); auto const &inserted = clauses.back(); for (auto var : inserted->solutions) (*this)[var].rclauses.push_back(inserted.get()); return inserted.get(); } void APT::Solver::Discover(Var var) { assert(discoverQ.empty()); discoverQ.push(var); while (not discoverQ.empty()) { var = discoverQ.front(); discoverQ.pop(); // Package needs to be discovered before the version to be able to dedup shared dependencies if (auto Ver = var.Ver(cache); not Ver.end() && not(*this)[Ver.ParentPkg()].flags.discovered) var = Var(Ver.ParentPkg()); auto &state = (*this)[var]; if (state.flags.discovered) continue; state.flags.discovered = true; if (auto Pkg = var.Pkg(cache); not Pkg.end()) { Clause clause{Var(Pkg), Group::SelectVersion}; for (auto ver = Pkg.VersionList(); not ver.end(); ver++) clause.solutions.push_back(Var(ver)); std::stable_sort(clause.solutions.begin(), clause.solutions.end(), CompareProviders3{cache, policy, Pkg, *this}); RegisterClause(std::move(clause)); RegisterCommonDependencies(Pkg); } else if (auto Ver = var.Ver(cache); not Ver.end()) { Clause clause{Var(Ver), Group::SelectVersion}; clause.solutions = {Var(Ver.ParentPkg())}; RegisterClause(std::move(clause)); for (auto OV = Ver.ParentPkg().VersionList(); not OV.end(); ++OV) { if (OV == Ver) continue; Clause clause{Var(Ver), Group::SelectVersion, false, true /* negative */}; clause.solutions = {Var(OV)}; RegisterClause(std::move(clause)); } for (auto dep = Ver.DependsList(); not dep.end();) { // Compute a single dependency element (glob or) pkgCache::DepIterator start; pkgCache::DepIterator end; dep.GlobOr(start, end); // advances dep // This dependency is shared across all versions, skip it. if (auto &pkgClauses = (*this)[Ver.ParentPkg()].clauses; std::any_of(pkgClauses.begin(), pkgClauses.end(), [this, start](auto &c) { return c->dep && SameOrGroup(start, pkgCache::DepIterator(cache, c->dep)); })) continue; auto clause = TranslateOrGroup(start, end, Var(Ver)); RegisterClause(std::move(clause)); } } // Recursively discover everything else that is not already FALSE by fact (MUSTNOT at depth 0) for (auto const &clause : state.clauses) for (auto const &var : clause->solutions) if ((*this)[var].decision != Decision::MUSTNOT || (*this)[var].depth > 0) discoverQ.push(var); } } void APT::Solver::RegisterCommonDependencies(pkgCache::PkgIterator Pkg) { for (auto dep = Pkg.VersionList().DependsList(); not dep.end();) { pkgCache::DepIterator start, end; dep.GlobOr(start, end); // advances dep bool allHaveDep = true; for (auto ver = Pkg.VersionList()++; allHaveDep && not ver.end(); ver++) { bool haveDep = false; for (auto otherDep = ver.DependsList(); not haveDep && not otherDep.end();) { pkgCache::DepIterator otherStart, otherEnd; otherDep.GlobOr(otherStart, otherEnd); // advances other dep haveDep = SameOrGroup(start, otherStart); } if (not haveDep) allHaveDep = false; } if (not allHaveDep) continue; auto clause = TranslateOrGroup(start, end, Var(Pkg)); RegisterClause(std::move(clause)); } } APT::Solver::Clause APT::Solver::TranslateOrGroup(pkgCache::DepIterator start, pkgCache::DepIterator end, Var reason) { auto TgtPkg = start.TargetPkg(); // Non-important dependencies can only be installed if they are currently satisfied, see the check further // below once we have calculated all possible solutions. if (start.ParentPkg()->CurrentVer == 0 && not policy.IsImportantDep(start)) return Clause{reason, Group::Satisfy, true}; // Replaces and Enhances are not a real dependency. if (start->Type == pkgCache::Dep::Replaces || start->Type == pkgCache::Dep::Enhances) return Clause{reason, Group::Satisfy, true}; if (unlikely(debug >= 3)) std::cerr << "Found dependency critical " << reason.toString(cache) << " -> " << start.TargetPkg().FullName() << "\n"; Clause clause{reason, Group::Satisfy, not start.IsCritical() /* optional */, start.IsNegative()}; clause.dep = start; do { auto begin = clause.solutions.size(); if (DeferVersionSelection && not start.IsNegative() && start.TargetPkg().ProvidesList().end() && start.IsSatisfied(start.TargetPkg())) { clause.solutions.push_back(Var(start.TargetPkg())); } else { auto all = start.AllTargets(); for (auto tgt = all; *tgt; ++tgt) { pkgCache::VerIterator tgti(cache, *tgt); if (unlikely(debug >= 3)) std::cerr << "Adding work to item " << reason.toString(cache) << " -> " << tgti.ParentPkg().FullName() << "=" << tgti.VerStr() << (clause.negative ? " (negative)" : "") << "\n"; clause.solutions.push_back(Var(pkgCache::VerIterator(cache, *tgt))); } delete[] all; std::stable_sort(clause.solutions.begin() + begin, clause.solutions.end(), CompareProviders3{cache, policy, TgtPkg, *this}); } if (start == end) break; ++start; } while (1); // Move obsolete packages to the end, and (non-obsolete) installed packages to the front if (not FixPolicyBroken) std::stable_sort(clause.solutions.begin(), clause.solutions.end(), [this](Var a, Var b) { if (IsUpgrade) if (auto obsoleteA = Obsolete(a.CastPkg(cache)), obsoleteB = Obsolete(b.CastPkg(cache)); obsoleteA != obsoleteB) return obsoleteB; if ((a.CastPkg(cache)->CurrentVer == 0 || b.CastPkg(cache)->CurrentVer == 0) && a.CastPkg(cache)->CurrentVer != b.CastPkg(cache)->CurrentVer) return a.CastPkg(cache)->CurrentVer != 0; return false; }); if (std::all_of(clause.solutions.begin(), clause.solutions.end(), [this](auto var) -> auto { return var.CastPkg(cache)->CurrentVer == 0; })) clause.group = Group::SatisfyNew; if (std::any_of(clause.solutions.begin(), clause.solutions.end(), [this](auto var) -> auto { return Obsolete(var.CastPkg(cache), true); })) clause.group = Group::SatisfyObsolete; // Try to perserve satisfied Recommends. FIXME: We should check if the Recommends was there in the installed version? if (clause.optional && start.ParentPkg()->CurrentVer) { bool important = policy.IsImportantDep(start); auto importantToKeep = [this](pkgCache::DepIterator d) { return policy.IsImportantDep(d) || (KeepRecommends && d->Type == pkgCache::Dep::Recommends) || (KeepSuggests && d->Type == pkgCache::Dep::Suggests); }; bool satisfied = std::any_of(clause.solutions.begin(), clause.solutions.end(), [this](auto var) { return var.Pkg(cache) ? var.Pkg(cache)->CurrentVer != nullptr : Var(var.CastPkg(cache).CurrentVer()) == var; }); // Find the existing dependency pkgCache::DepIterator existing; for (auto D = start.ParentPkg().CurrentVer().DependsList(); not D.end(); D++) if (not D.IsCritical() && importantToKeep(D) && D.TargetPkg() == start.TargetPkg()) { existing = D; break; } if (not existing.end() && not important && importantToKeep(start) && satisfied) { if (unlikely(debug >= 3)) std::cerr << "Try to keep satisfied: " << clause.toString(cache, true) << std::endl; clause.group = Group::SatisfySuggests; // Erase the non-installed solutions. We will process this last and try to keep the previously installed // "best" solution installed. clause.solutions.erase(std::remove_if(clause.solutions.begin(), clause.solutions.end(), [this](auto var) { return var.CastPkg(cache)->CurrentVer == nullptr; }), clause.solutions.end()); } else if (not important) { if (unlikely(debug >= 3)) std::cerr << "Ignore unimportant clause: " << clause.toString(cache, true) << std::endl; return Clause{reason, Group::Satisfy, true}; } else if (not existing.end() && policy.IsImportantDep(existing) && not satisfied) { if (unlikely(debug >= 3)) std::cerr << "Ignoring unsatisfied clause: " << clause.toString(cache, true) << std::endl; return Clause{reason, Group::Satisfy, true}; } else if (IsUpgrade && not existing.end() && satisfied) { if (unlikely(debug >= 3)) std::cerr << "Promoting previously satisfied clause to hard dependency: " << clause.toString(cache, true) << std::endl; clause.optional = false; } else if ( IsUpgrade && not(AllowRemove && AllowInstall) // promote Recommends to Depends in upgrade, not in dist-upgrade. && reason.Ver() && reason.Ver(cache) != reason.CastPkg(cache).CurrentVer() // and if this an upgrade to an installed package && (existing.end() || not policy.IsImportantDep(existing)) // new Recommends, or upgraded from Suggests ) { if (unlikely(debug >= 3)) std::cerr << "Promoting new clause to hard dependency: " << clause.toString(cache) << std::endl; clause.optional = false; } } return clause; } void APT::Solver::Push(Work work) { if (unlikely(debug >= 2)) std::cerr << "Trying choice for " << work.toString(cache) << std::endl; choices.push_back(solved.size()); solved.push_back(Solved{Var(), std::move(work)}); } void APT::Solver::UndoOne() { auto solvedItem = solved.back(); if (unlikely(debug >= 4)) std::cerr << "Undoing a single decision\n"; if (not solvedItem.assigned.empty()) { if (unlikely(debug >= 4)) std::cerr << "Unassign " << solvedItem.assigned.toString(cache) << "\n"; auto &state = (*this)[solvedItem.assigned]; state.decision = Decision::NONE; state.reason = nullptr; state.depth = 0; } if (auto work = solvedItem.work) { if (unlikely(debug >= 4)) std::cerr << "Adding work item " << work->toString(cache) << std::endl; if (not AddWork(std::move(*work))) abort(); } solved.pop_back(); // FIXME: Add the undo handling here once we have watchers. } bool APT::Solver::Pop() { if (depth() == 0) return false; time_t now = time(nullptr); if (now - startTime >= Timeout) return _error->Error("Solver timed out."); if (unlikely(debug >= 2)) for (std::string msg; _error->PopMessage(msg);) std::cerr << "Branch failed: " << msg << std::endl; _error->Discard(); assert(choices.back() < solved.size()); int itemsToUndo = solved.size() - choices.back(); auto choice = solved[choices.back()].work->choice; for (; itemsToUndo; --itemsToUndo) UndoOne(); // We need to remove any work that is at a higher depth. // FIXME: We should just mark the entries as erased and only do a compaction // of the heap once we have a lot of erased entries in it. choices.pop_back(); work.erase(std::remove_if(work.begin(), work.end(), [this](Work &w) -> bool { return w.depth > depth() || w.erased; }), work.end()); std::make_heap(work.begin(), work.end()); if (unlikely(debug >= 2)) std::cerr << "Backtracking to choice " << choice.toString(cache) << "\n"; // FIXME: There should be a reason! if (not Enqueue(choice, false, {})) return false; if (unlikely(debug >= 2)) std::cerr << "Backtracked to choice " << choice.toString(cache) << "\n"; return true; } bool APT::Solver::AddWork(Work &&w) { if (w.clause->negative) { for (auto var : w.clause->solutions) if (not Enqueue(var, false, w.clause)) return false; } else { if (unlikely(debug >= 3 && w.clause->optional)) std::cerr << "Enqueuing Recommends " << w.clause->toString(cache) << std::endl; if (w.clause->solutions.size() == 1 && not w.clause->optional) return Enqueue(w.clause->solutions[0], true, w.clause); w.size = std::count_if(w.clause->solutions.begin(), w.clause->solutions.end(), [this](auto V) { return (*this)[V].decision != Decision::MUSTNOT; }); work.push_back(std::move(w)); std::push_heap(work.begin(), work.end()); } return true; } bool APT::Solver::Solve() { _error->PushToStack(); DEFER([&]() { _error->MergeWithStack(); }); startTime = time(nullptr); while (true) { while (not Propagate()) { if (not Pop()) return false; } if (work.empty()) break; // *NOW* we can pop the item. std::pop_heap(work.begin(), work.end()); // This item has been replaced with a new one. Remove it. if (work.back().erased) { work.pop_back(); continue; } auto item = std::move(work.back()); work.pop_back(); solved.push_back(Solved{Var(), item}); if (std::any_of(item.clause->solutions.begin(), item.clause->solutions.end(), [this](auto ver) { return (*this)[ver].decision == Decision::MUST; })) { if (unlikely(debug >= 2)) std::cerr << "ELIDED " << item.toString(cache) << std::endl; continue; } if (unlikely(debug >= 1)) std::cerr << item.toString(cache) << std::endl; bool foundSolution = false; for (auto &sol : item.clause->solutions) { if ((*this)[sol].decision == Decision::MUSTNOT) { if (unlikely(debug >= 3)) std::cerr << "(existing conflict: " << sol.toString(cache) << ")\n"; continue; } if (item.size > 1 || item.clause->optional) { item.choice = sol; Push(item); } if (unlikely(debug >= 3)) std::cerr << "(try it: " << sol.toString(cache) << ")\n"; if (not Enqueue(sol, true, item.clause) && not Pop()) return false; foundSolution = true; break; } if (not foundSolution && not item.clause->optional) { std::ostringstream err; err << "Unable to satisfy dependencies. Reached two conflicting decisions:" << "\n"; std::unordered_set seen; err << "1. " << LongWhyStr(item.clause->reason, true, (*this)[item.clause->reason].reason, " ", seen).substr(3) << "\n"; err << "2. " << LongWhyStr(item.clause->reason, false, item.clause, " ", seen).substr(3); _error->Error("%s", err.str().c_str()); if (not Pop()) return false; } } return true; } // \brief Apply the selections from the dep cache to the solver bool APT::Solver::FromDepCache(pkgDepCache &depcache) { DefaultRootSetFunc2 rootSet(&cache); // Enforce strict pinning rules by rejecting all forbidden versions. if (StrictPinning) { for (auto P = cache.PkgBegin(); not P.end(); P++) { bool isForced = depcache[P].Protect() && depcache[P].Install(); bool isPhasing = IsUpgrade && depcache.PhasingApplied(P) && not isForced; for (auto V = P.VersionList(); not V.end(); ++V) if (P.CurrentVer() != V && (depcache.GetCandidateVersion(P) != V || isPhasing)) if (not Enqueue(Var(V), false, {})) return false; } } // Clause discovery depends on the manual flag, so we need to set the manual flag first before we discover any packages for (auto P = cache.PkgBegin(); not P.end(); P++) if (P->CurrentVer && not(depcache[P].Flags & pkgCache::Flag::Auto) && (depcache[P].Keep() || depcache[P].Install())) (*this)[P].flags.manual = true; for (auto P = cache.PkgBegin(); not P.end(); P++) { if (P->VersionList == nullptr) continue; auto state = depcache[P]; if (P->SelectedState == pkgCache::State::Hold && not state.Protect()) { if (unlikely(debug >= 1)) std::cerr << "Hold " << P.FullName() << "\n"; if (P->CurrentVer ? not Enqueue(Var(P.CurrentVer()), true) : not Enqueue(Var(P), false)) return false; } else if (state.Delete() // Normal delete request. || (not P->CurrentVer && state.Keep() && state.Protect()) // Delete request of not installed package. || (not P->CurrentVer && state.Keep() && not AllowInstall) // New package installs not allowed. ) { if (unlikely(debug >= 1)) std::cerr << "Delete " << P.FullName() << "\n"; if (not Enqueue(Var(P), false)) return false; } else if (state.Install() || (state.Keep() && P->CurrentVer)) { auto isEssential = P->Flags & (pkgCache::Flag::Essential | pkgCache::Flag::Important); auto isAuto = (depcache[P].Flags & pkgCache::Flag::Auto); auto isOptional = ((isAuto && AllowRemove) || AllowRemoveManual) && not isEssential && not depcache[P].Protect(); auto Root = rootSet.InRootSet(P); auto Upgrade = depcache.GetCandidateVersion(P) != P.CurrentVer(); auto Group = isAuto ? (Upgrade ? Group::UpgradeAuto : Group::KeepAuto) : (Upgrade ? Group::UpgradeManual : Group::InstallManual); if (isAuto && not depcache[P].Protect() && not isEssential && not KeepAuto && not rootSet.InRootSet(P)) { if (unlikely(debug >= 1)) std::cerr << "Ignore automatic install " << P.FullName() << " (" << (isEssential ? "E" : "") << (isAuto ? "M" : "") << (Root ? "R" : "") << ")" << "\n"; continue; } if (unlikely(debug >= 1)) std::cerr << "Install " << P.FullName() << " (" << (isEssential ? "E" : "") << (isAuto ? "M" : "") << (Root ? "R" : "") << ")" << "\n"; if (not isOptional) { // Pre-empt the non-optional requests, as we don't want to queue them, we can just "unit propagate" here. if (depcache[P].Keep() ? not Enqueue(Var(P), true) : not Enqueue(Var(depcache.GetCandidateVersion(P)), true)) return false; } else { Clause w{Var(), Group, isOptional}; w.solutions.push_back(Var(P)); auto insertedW = RegisterClause(std::move(w)); if (not AddWork(Work{insertedW, depth()})) return false; // Given A->A2|A1, B->B1|B2; Bn->An, if we select `not A1`, we // should try to install A2 before trying B so we end up with // A2, B2, instead of removing A1 to keep B1 installed. This // requires some special casing in Work::operator< above. // Compare test-bug-712116-dpkg-pre-install-pkgs-hook-multiarch Clause shortcircuit{Var(), Group, isOptional}; for (auto V = P.VersionList(); not V.end(); ++V) shortcircuit.solutions.push_back(Var(V)); std::stable_sort(shortcircuit.solutions.begin(), shortcircuit.solutions.end(), CompareProviders3{cache, policy, P, *this}); auto insertedShort = RegisterClause(std::move(shortcircuit)); if (not AddWork(Work{insertedShort, depth()})) return false; // Discovery here is needed so the shortcircuit clause can actually become unit. if (P.VersionList() && P.VersionList()->NextVer) Discover(Var(P)); } } else if (IsUpgrade && AllowRemove && AllowInstall && (P->Flags & pkgCache::Flag::Essential)) { Clause w{Var(), Group::InstallManual, false}; auto G = P.Group(); for (auto P = G.PackageList(); not P.end(); P = G.NextPkg(P)) if (P->Flags & pkgCache::Flag::Essential) w.solutions.push_back(Var(P)); std::stable_sort(w.solutions.begin(), w.solutions.end(), CompareProviders3{cache, policy, P, *this}); if (unlikely(debug >= 1)) std::cerr << "Install essential package " << P << std::endl; auto inserted = RegisterClause(std::move(w)); if (not AddWork(Work{inserted, depth()})) return false; } } return Propagate(); } bool APT::Solver::ToDepCache(pkgDepCache &depcache) const { FastContiguousCacheMap movedManual(cache); pkgDepCache::ActionGroup group(depcache); for (auto P = cache.PkgBegin(); not P.end(); P++) { depcache[P].Marked = 0; depcache[P].Garbage = 0; if ((*this)[P].decision == Decision::MUST) { pkgCache::VerIterator cand; for (auto V = P.VersionList(); cand.end() && not V.end(); V++) if ((*this)[V].decision == Decision::MUST) cand = V; auto reasonClause = (*this)[cand].reason; auto reason = reasonClause ? reasonClause->reason : Var(); if (auto RP = reason.Pkg(); RP == P.MapPointer()) reason = (*this)[P].reason ? (*this)[P].reason->reason : Var(); if (cand != P.CurrentVer()) { bool automatic = (not reason.empty() || (depcache[P].Flags & pkgCache::Flag::Auto)) && not movedManual[P]; depcache.SetCandidateVersion(cand); depcache.MarkInstall(P, false, 0, not automatic); // Set the automatic bit for new packages or move it on upgrades to oldlibs if (not P->CurrentVer) depcache.MarkAuto(P, automatic); else if (not(depcache[P].Flags & pkgCache::Flag::Auto) && P.CurrentVer()->Section && cand->Section && not _config->SectionInSubTree("APT::Move-Autobit-Sections", P.CurrentVer().Section()) && _config->SectionInSubTree("APT::Move-Autobit-Sections", cand.Section())) { bool moved = false; for (auto const &clause : (*this)[cand].clauses) for (auto sol : clause->solutions) { // New installs move the auto-bit. TODO: Should we look at whether clause is the reason for installing it? if (sol.CastPkg(cache) == P || sol.CastPkg(cache)->CurrentVer) continue; std::cerr << "Move manual bit from " << P.FullName() << " to " << sol.CastPkg(cache).Name() << std::endl; movedManual[sol.CastPkg(cache)] = true; depcache.MarkAuto(sol.CastPkg(cache), false); moved = true; } if (moved) depcache.MarkAuto(P, true); } } else depcache.MarkKeep(P, false, reason.empty() && not(depcache[P].Flags & pkgCache::Flag::Auto)); depcache[P].Marked = 1; depcache[P].Garbage = 0; } else if (P->CurrentVer || depcache[P].Install()) { depcache.MarkDelete(P, false, 0, not(*this)[P].reason); depcache[P].Marked = 0; depcache[P].Garbage = 1; } } return true; }