123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847 |
- <?php
- /*
- * This file is part of Composer.
- *
- * (c) Nils Adermann <naderman@naderman.de>
- * Jordi Boggiano <j.boggiano@seld.be>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Composer\DependencyResolver;
- use Composer\Repository\RepositoryInterface;
- use Composer\Repository\PlatformRepository;
- /**
- * @author Nils Adermann <naderman@naderman.de>
- */
- class Solver
- {
- const BRANCH_LITERALS = 0;
- const BRANCH_LEVEL = 1;
- /** @var PolicyInterface */
- protected $policy;
- /** @var Pool */
- protected $pool;
- /** @var RepositoryInterface */
- protected $installed;
- /** @var Ruleset */
- protected $rules;
- /** @var RuleSetGenerator */
- protected $ruleSetGenerator;
- /** @var array */
- protected $jobs;
- /** @var int[] */
- protected $updateMap = array();
- /** @var RuleWatchGraph */
- protected $watchGraph;
- /** @var Decisions */
- protected $decisions;
- /** @var int[] */
- protected $installedMap;
- /** @var int */
- protected $propagateIndex;
- /** @var array[] */
- protected $branches = array();
- /** @var Problem[] */
- protected $problems = array();
- /** @var array */
- protected $learnedPool = array();
- /** @var array */
- protected $learnedWhy = array();
- /**
- * @param PolicyInterface $policy
- * @param Pool $pool
- * @param RepositoryInterface $installed
- */
- public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed)
- {
- $this->policy = $policy;
- $this->pool = $pool;
- $this->installed = $installed;
- $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool);
- }
- /**
- * @return int
- */
- public function getRuleSetSize()
- {
- return count($this->rules);
- }
- // aka solver_makeruledecisions
- private function makeAssertionRuleDecisions()
- {
- $decisionStart = count($this->decisions) - 1;
- $rulesCount = count($this->rules);
- for ($ruleIndex = 0; $ruleIndex < $rulesCount; $ruleIndex++) {
- $rule = $this->rules->ruleById[$ruleIndex];
- if (!$rule->isAssertion() || $rule->isDisabled()) {
- continue;
- }
- $literals = $rule->literals;
- $literal = $literals[0];
- if (!$this->decisions->decided(abs($literal))) {
- $this->decisions->decide($literal, 1, $rule);
- continue;
- }
- if ($this->decisions->satisfy($literal)) {
- continue;
- }
- // found a conflict
- if (RuleSet::TYPE_LEARNED === $rule->getType()) {
- $rule->disable();
- continue;
- }
- $conflict = $this->decisions->decisionRule($literal);
- if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
- $problem = new Problem($this->pool);
- $problem->addRule($rule);
- $problem->addRule($conflict);
- $this->disableProblem($rule);
- $this->problems[] = $problem;
- continue;
- }
- // conflict with another job
- $problem = new Problem($this->pool);
- $problem->addRule($rule);
- $problem->addRule($conflict);
- // push all of our rules (can only be job rules)
- // asserting this literal on the problem stack
- foreach ($this->rules->getIteratorFor(RuleSet::TYPE_JOB) as $assertRule) {
- if ($assertRule->isDisabled() || !$assertRule->isAssertion()) {
- continue;
- }
- $assertRuleLiterals = $assertRule->literals;
- $assertRuleLiteral = $assertRuleLiterals[0];
- if (abs($literal) !== abs($assertRuleLiteral)) {
- continue;
- }
- $problem->addRule($assertRule);
- $this->disableProblem($assertRule);
- }
- $this->problems[] = $problem;
- $this->decisions->resetToOffset($decisionStart);
- $ruleIndex = -1;
- }
- }
- protected function setupInstalledMap()
- {
- $this->installedMap = array();
- foreach ($this->installed->getPackages() as $package) {
- $this->installedMap[$package->id] = $package;
- }
- }
- /**
- * @param bool $ignorePlatformReqs
- */
- protected function checkForRootRequireProblems($ignorePlatformReqs)
- {
- foreach ($this->jobs as $job) {
- switch ($job['cmd']) {
- case 'update':
- $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
- foreach ($packages as $package) {
- if (isset($this->installedMap[$package->id])) {
- $this->updateMap[$package->id] = true;
- }
- }
- break;
- case 'update-all':
- foreach ($this->installedMap as $package) {
- $this->updateMap[$package->id] = true;
- }
- break;
- case 'install':
- if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
- break;
- }
- if (!$this->pool->whatProvides($job['packageName'], $job['constraint'])) {
- $problem = new Problem($this->pool);
- $problem->addRule(new Rule(array(), null, null, $job));
- $this->problems[] = $problem;
- }
- break;
- }
- }
- }
- /**
- * @param Request $request
- * @param bool $ignorePlatformReqs
- * @return array
- */
- public function solve(Request $request, $ignorePlatformReqs = false)
- {
- $this->jobs = $request->getJobs();
- $this->setupInstalledMap();
- $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs);
- $this->checkForRootRequireProblems($ignorePlatformReqs);
- $this->decisions = new Decisions($this->pool);
- $this->watchGraph = new RuleWatchGraph;
- foreach ($this->rules as $rule) {
- $this->watchGraph->insert(new RuleWatchNode($rule));
- }
- /* make decisions based on job/update assertions */
- $this->makeAssertionRuleDecisions();
- $this->runSat(true);
- // decide to remove everything that's installed and undecided
- foreach ($this->installedMap as $packageId => $void) {
- if ($this->decisions->undecided($packageId)) {
- $this->decisions->decide(-$packageId, 1, null);
- }
- }
- if ($this->problems) {
- throw new SolverProblemsException($this->problems, $this->installedMap);
- }
- $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
- return $transaction->getOperations();
- }
- /**
- * Makes a decision and propagates it to all rules.
- *
- * Evaluates each term affected by the decision (linked through watches)
- * If we find unit rules we make new decisions based on them
- *
- * @param int $level
- * @return Rule|null A rule on conflict, otherwise null.
- */
- protected function propagate($level)
- {
- while ($this->decisions->validOffset($this->propagateIndex)) {
- $decision = $this->decisions->atOffset($this->propagateIndex);
- $conflict = $this->watchGraph->propagateLiteral(
- $decision[Decisions::DECISION_LITERAL],
- $level,
- $this->decisions
- );
- $this->propagateIndex++;
- if ($conflict) {
- return $conflict;
- }
- }
- return null;
- }
- /**
- * Reverts a decision at the given level.
- *
- * @param int $level
- */
- private function revert($level)
- {
- while (!$this->decisions->isEmpty()) {
- $literal = $this->decisions->lastLiteral();
- if ($this->decisions->undecided($literal)) {
- break;
- }
- $decisionLevel = $this->decisions->decisionLevel($literal);
- if ($decisionLevel <= $level) {
- break;
- }
- $this->decisions->revertLast();
- $this->propagateIndex = count($this->decisions);
- }
- while (!empty($this->branches) && $this->branches[count($this->branches) - 1][self::BRANCH_LEVEL] >= $level) {
- array_pop($this->branches);
- }
- }
- /**
- * setpropagatelearn
- *
- * add free decision (a positive literal) to decision queue
- * increase level and propagate decision
- * return if no conflict.
- *
- * in conflict case, analyze conflict rule, add resulting
- * rule to learnt rule set, make decision from learnt
- * rule (always unit) and re-propagate.
- *
- * returns the new solver level or 0 if unsolvable
- *
- * @param int $level
- * @param string|int $literal
- * @param bool $disableRules
- * @param Rule $rule
- * @return int
- */
- private function setPropagateLearn($level, $literal, $disableRules, Rule $rule)
- {
- $level++;
- $this->decisions->decide($literal, $level, $rule);
- while (true) {
- $rule = $this->propagate($level);
- if (!$rule) {
- break;
- }
- if ($level == 1) {
- return $this->analyzeUnsolvable($rule, $disableRules);
- }
- // conflict
- list($learnLiteral, $newLevel, $newRule, $why) = $this->analyze($level, $rule);
- if ($newLevel <= 0 || $newLevel >= $level) {
- throw new SolverBugException(
- "Trying to revert to invalid level ".(int) $newLevel." from level ".(int) $level."."
- );
- } elseif (!$newRule) {
- throw new SolverBugException(
- "No rule was learned from analyzing $rule at level $level."
- );
- }
- $level = $newLevel;
- $this->revert($level);
- $this->rules->add($newRule, RuleSet::TYPE_LEARNED);
- $this->learnedWhy[spl_object_hash($newRule)] = $why;
- $ruleNode = new RuleWatchNode($newRule);
- $ruleNode->watch2OnHighest($this->decisions);
- $this->watchGraph->insert($ruleNode);
- $this->decisions->decide($learnLiteral, $level, $newRule);
- }
- return $level;
- }
- /**
- * @param int $level
- * @param array $decisionQueue
- * @param bool $disableRules
- * @param Rule $rule
- * @return int
- */
- private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule)
- {
- // choose best package to install from decisionQueue
- $literals = $this->policy->selectPreferredPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage());
- $selectedLiteral = array_shift($literals);
- // if there are multiple candidates, then branch
- if (count($literals)) {
- $this->branches[] = array($literals, $level);
- }
- return $this->setPropagateLearn($level, $selectedLiteral, $disableRules, $rule);
- }
- /**
- * @param int $level
- * @param Rule $rule
- * @return array
- */
- protected function analyze($level, Rule $rule)
- {
- $analyzedRule = $rule;
- $ruleLevel = 1;
- $num = 0;
- $l1num = 0;
- $seen = array();
- $learnedLiterals = array(null);
- $decisionId = count($this->decisions);
- $this->learnedPool[] = array();
- while (true) {
- $this->learnedPool[count($this->learnedPool) - 1][] = $rule;
- foreach ($rule->literals as $literal) {
- // skip the one true literal
- if ($this->decisions->satisfy($literal)) {
- continue;
- }
- if (isset($seen[abs($literal)])) {
- continue;
- }
- $seen[abs($literal)] = true;
- $l = $this->decisions->decisionLevel($literal);
- if (1 === $l) {
- $l1num++;
- } elseif ($level === $l) {
- $num++;
- } else {
- // not level1 or conflict level, add to new rule
- $learnedLiterals[] = $literal;
- if ($l > $ruleLevel) {
- $ruleLevel = $l;
- }
- }
- }
- $l1retry = true;
- while ($l1retry) {
- $l1retry = false;
- if (!$num && !--$l1num) {
- // all level 1 literals done
- break 2;
- }
- while (true) {
- if ($decisionId <= 0) {
- throw new SolverBugException(
- "Reached invalid decision id $decisionId while looking through $rule for a literal present in the analyzed rule $analyzedRule."
- );
- }
- $decisionId--;
- $decision = $this->decisions->atOffset($decisionId);
- $literal = $decision[Decisions::DECISION_LITERAL];
- if (isset($seen[abs($literal)])) {
- break;
- }
- }
- unset($seen[abs($literal)]);
- if ($num && 0 === --$num) {
- $learnedLiterals[0] = -abs($literal);
- if (!$l1num) {
- break 2;
- }
- foreach ($learnedLiterals as $i => $learnedLiteral) {
- if ($i !== 0) {
- unset($seen[abs($learnedLiteral)]);
- }
- }
- // only level 1 marks left
- $l1num++;
- $l1retry = true;
- }
- }
- $decision = $this->decisions->atOffset($decisionId);
- $rule = $decision[Decisions::DECISION_REASON];
- }
- $why = count($this->learnedPool) - 1;
- if (!$learnedLiterals[0]) {
- throw new SolverBugException(
- "Did not find a learnable literal in analyzed rule $analyzedRule."
- );
- }
- $newRule = new Rule($learnedLiterals, Rule::RULE_LEARNED, $why);
- return array($learnedLiterals[0], $ruleLevel, $newRule, $why);
- }
- /**
- * @param Problem $problem
- * @param Rule $conflictRule
- */
- private function analyzeUnsolvableRule(Problem $problem, Rule $conflictRule)
- {
- $why = spl_object_hash($conflictRule);
- if ($conflictRule->getType() == RuleSet::TYPE_LEARNED) {
- $learnedWhy = $this->learnedWhy[$why];
- $problemRules = $this->learnedPool[$learnedWhy];
- foreach ($problemRules as $problemRule) {
- $this->analyzeUnsolvableRule($problem, $problemRule);
- }
- return;
- }
- if ($conflictRule->getType() == RuleSet::TYPE_PACKAGE) {
- // package rules cannot be part of a problem
- return;
- }
- $problem->nextSection();
- $problem->addRule($conflictRule);
- }
- /**
- * @param Rule $conflictRule
- * @param bool $disableRules
- * @return int
- */
- private function analyzeUnsolvable(Rule $conflictRule, $disableRules)
- {
- $problem = new Problem($this->pool);
- $problem->addRule($conflictRule);
- $this->analyzeUnsolvableRule($problem, $conflictRule);
- $this->problems[] = $problem;
- $seen = array();
- $literals = $conflictRule->literals;
- foreach ($literals as $literal) {
- // skip the one true literal
- if ($this->decisions->satisfy($literal)) {
- continue;
- }
- $seen[abs($literal)] = true;
- }
- foreach ($this->decisions as $decision) {
- $literal = $decision[Decisions::DECISION_LITERAL];
- // skip literals that are not in this rule
- if (!isset($seen[abs($literal)])) {
- continue;
- }
- $why = $decision[Decisions::DECISION_REASON];
- $problem->addRule($why);
- $this->analyzeUnsolvableRule($problem, $why);
- $literals = $why->literals;
- foreach ($literals as $literal) {
- // skip the one true literal
- if ($this->decisions->satisfy($literal)) {
- continue;
- }
- $seen[abs($literal)] = true;
- }
- }
- if ($disableRules) {
- foreach ($this->problems[count($this->problems) - 1] as $reason) {
- $this->disableProblem($reason['rule']);
- }
- $this->resetSolver();
- return 1;
- }
- return 0;
- }
- /**
- * @param Rule $why
- */
- private function disableProblem(Rule $why)
- {
- $job = $why->getJob();
- if (!$job) {
- $why->disable();
- return;
- }
- // disable all rules of this job
- foreach ($this->rules as $rule) {
- /** @var Rule $rule */
- if ($job === $rule->getJob()) {
- $rule->disable();
- }
- }
- }
- private function resetSolver()
- {
- $this->decisions->reset();
- $this->propagateIndex = 0;
- $this->branches = array();
- $this->enableDisableLearnedRules();
- $this->makeAssertionRuleDecisions();
- }
- /**
- * enable/disable learnt rules
- *
- * we have enabled or disabled some of our rules. We now re-enable all
- * of our learnt rules except the ones that were learnt from rules that
- * are now disabled.
- */
- private function enableDisableLearnedRules()
- {
- foreach ($this->rules->getIteratorFor(RuleSet::TYPE_LEARNED) as $rule) {
- $why = $this->learnedWhy[spl_object_hash($rule)];
- $problemRules = $this->learnedPool[$why];
- $foundDisabled = false;
- foreach ($problemRules as $problemRule) {
- if ($problemRule->isDisabled()) {
- $foundDisabled = true;
- break;
- }
- }
- if ($foundDisabled && $rule->isEnabled()) {
- $rule->disable();
- } elseif (!$foundDisabled && $rule->isDisabled()) {
- $rule->enable();
- }
- }
- }
- /**
- * @param bool $disableRules
- */
- private function runSat($disableRules = true)
- {
- $this->propagateIndex = 0;
- /*
- * here's the main loop:
- * 1) propagate new decisions (only needed once)
- * 2) fulfill jobs
- * 3) fulfill all unresolved rules
- * 4) minimalize solution if we had choices
- * if we encounter a problem, we rewind to a safe level and restart
- * with step 1
- */
- $decisionQueue = array();
- $decisionSupplementQueue = array();
- /**
- * @todo this makes $disableRules always false; determine the rationale and possibly remove dead code?
- */
- $disableRules = array();
- $level = 1;
- $systemLevel = $level + 1;
- $installedPos = 0;
- while (true) {
- if (1 === $level) {
- $conflictRule = $this->propagate($level);
- if (null !== $conflictRule) {
- if ($this->analyzeUnsolvable($conflictRule, $disableRules)) {
- continue;
- }
- return;
- }
- }
- // handle job rules
- if ($level < $systemLevel) {
- $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_JOB);
- foreach ($iterator as $rule) {
- if ($rule->isEnabled()) {
- $decisionQueue = array();
- $noneSatisfied = true;
- foreach ($rule->literals as $literal) {
- if ($this->decisions->satisfy($literal)) {
- $noneSatisfied = false;
- break;
- }
- if ($literal > 0 && $this->decisions->undecided($literal)) {
- $decisionQueue[] = $literal;
- }
- }
- if ($noneSatisfied && count($decisionQueue)) {
- // prune all update packages until installed version
- // except for requested updates
- if (count($this->installed) != count($this->updateMap)) {
- $prunedQueue = array();
- foreach ($decisionQueue as $literal) {
- if (isset($this->installedMap[abs($literal)])) {
- $prunedQueue[] = $literal;
- if (isset($this->updateMap[abs($literal)])) {
- $prunedQueue = $decisionQueue;
- break;
- }
- }
- }
- $decisionQueue = $prunedQueue;
- }
- }
- if ($noneSatisfied && count($decisionQueue)) {
- $oLevel = $level;
- $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
- if (0 === $level) {
- return;
- }
- if ($level <= $oLevel) {
- break;
- }
- }
- }
- }
- $systemLevel = $level + 1;
- // jobs left
- $iterator->next();
- if ($iterator->valid()) {
- continue;
- }
- }
- if ($level < $systemLevel) {
- $systemLevel = $level;
- }
- for ($i = 0, $n = 0; $n < count($this->rules); $i++, $n++) {
- if ($i == count($this->rules)) {
- $i = 0;
- }
- $rule = $this->rules->ruleById[$i];
- $literals = $rule->literals;
- if ($rule->isDisabled()) {
- continue;
- }
- $decisionQueue = array();
- // make sure that
- // * all negative literals are installed
- // * no positive literal is installed
- // i.e. the rule is not fulfilled and we
- // just need to decide on the positive literals
- //
- foreach ($literals as $literal) {
- if ($literal <= 0) {
- if (!$this->decisions->decidedInstall(abs($literal))) {
- continue 2; // next rule
- }
- } else {
- if ($this->decisions->decidedInstall(abs($literal))) {
- continue 2; // next rule
- }
- if ($this->decisions->undecided(abs($literal))) {
- $decisionQueue[] = $literal;
- }
- }
- }
- // need to have at least 2 item to pick from
- if (count($decisionQueue) < 2) {
- continue;
- }
- $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
- if (0 === $level) {
- return;
- }
- // something changed, so look at all rules again
- $n = -1;
- }
- if ($level < $systemLevel) {
- continue;
- }
- // minimization step
- if (count($this->branches)) {
- $lastLiteral = null;
- $lastLevel = null;
- $lastBranchIndex = 0;
- $lastBranchOffset = 0;
- for ($i = count($this->branches) - 1; $i >= 0; $i--) {
- list($literals, $l) = $this->branches[$i];
- foreach ($literals as $offset => $literal) {
- if ($literal && $literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) {
- $lastLiteral = $literal;
- $lastBranchIndex = $i;
- $lastBranchOffset = $offset;
- $lastLevel = $l;
- }
- }
- }
- if ($lastLiteral) {
- unset($this->branches[$lastBranchIndex][self::BRANCH_LITERALS][$lastBranchOffset]);
- $level = $lastLevel;
- $this->revert($level);
- $why = $this->decisions->lastReason();
- $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why);
- if ($level == 0) {
- return;
- }
- continue;
- }
- }
- break;
- }
- }
- }
|