Merge remote-tracking branch 'naderman/solver-refactor'
commit
4ea9b33a6c
@ -0,0 +1,83 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nils Adermann <naderman@naderman.de>
|
||||||
|
*/
|
||||||
|
class DebugSolver extends Solver
|
||||||
|
{
|
||||||
|
protected function printDecisionMap()
|
||||||
|
{
|
||||||
|
echo "\nDecisionMap: \n";
|
||||||
|
foreach ($this->decisionMap as $packageId => $level) {
|
||||||
|
if ($packageId === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($level > 0) {
|
||||||
|
echo ' +' . $this->pool->packageById($packageId)."\n";
|
||||||
|
} elseif ($level < 0) {
|
||||||
|
echo ' -' . $this->pool->packageById($packageId)."\n";
|
||||||
|
} else {
|
||||||
|
echo ' ?' . $this->pool->packageById($packageId)."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function printDecisionQueue()
|
||||||
|
{
|
||||||
|
echo "DecisionQueue: \n";
|
||||||
|
foreach ($this->decisionQueue as $i => $literal) {
|
||||||
|
echo ' ' . $this->pool->literalToString($literal) . ' ' . $this->decisionQueueWhy[$i]." level ".$this->decisionMap[abs($literal)]."\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function printWatches()
|
||||||
|
{
|
||||||
|
echo "\nWatches:\n";
|
||||||
|
foreach ($this->watches as $literalId => $watch) {
|
||||||
|
echo ' '.$this->literalFromId($literalId)."\n";
|
||||||
|
$queue = array(array(' ', $watch));
|
||||||
|
|
||||||
|
while (!empty($queue)) {
|
||||||
|
list($indent, $watch) = array_pop($queue);
|
||||||
|
|
||||||
|
echo $indent.$watch;
|
||||||
|
|
||||||
|
if ($watch) {
|
||||||
|
echo ' [id='.$watch->getId().',watch1='.$this->literalFromId($watch->watch1).',watch2='.$this->literalFromId($watch->watch2)."]";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
if ($watch && ($watch->next1 == $watch || $watch->next2 == $watch)) {
|
||||||
|
if ($watch->next1 == $watch) {
|
||||||
|
echo $indent." 1 *RECURSION*";
|
||||||
|
}
|
||||||
|
if ($watch->next2 == $watch) {
|
||||||
|
echo $indent." 2 *RECURSION*";
|
||||||
|
}
|
||||||
|
} elseif ($watch && ($watch->next1 || $watch->next2)) {
|
||||||
|
$indent = str_replace(array('1', '2'), ' ', $indent);
|
||||||
|
|
||||||
|
array_push($queue, array($indent.' 2 ', $watch->next2));
|
||||||
|
array_push($queue, array($indent.' 1 ', $watch->next1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,67 +0,0 @@
|
|||||||
<?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\Package\PackageInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Nils Adermann <naderman@naderman.de>
|
|
||||||
*/
|
|
||||||
class Literal
|
|
||||||
{
|
|
||||||
protected $package;
|
|
||||||
protected $wanted;
|
|
||||||
protected $id;
|
|
||||||
|
|
||||||
public function __construct(PackageInterface $package, $wanted)
|
|
||||||
{
|
|
||||||
$this->package = $package;
|
|
||||||
$this->wanted = $wanted;
|
|
||||||
$this->id = ($this->wanted ? '' : '-') . $this->package->getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isWanted()
|
|
||||||
{
|
|
||||||
return $this->wanted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPackage()
|
|
||||||
{
|
|
||||||
return $this->package;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPackageId()
|
|
||||||
{
|
|
||||||
return $this->package->getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId()
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString()
|
|
||||||
{
|
|
||||||
return ($this->wanted ? '+' : '-') . $this->getPackage();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function inverted()
|
|
||||||
{
|
|
||||||
return new Literal($this->getPackage(), !$this->isWanted());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function equals(Literal $b)
|
|
||||||
{
|
|
||||||
return $this->id === $b->id;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,289 @@
|
|||||||
|
<?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\Package\PackageInterface;
|
||||||
|
use Composer\Package\AliasPackage;
|
||||||
|
use Composer\DependencyResolver\Operation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nils Adermann <naderman@naderman.de>
|
||||||
|
*/
|
||||||
|
class RuleSetGenerator
|
||||||
|
{
|
||||||
|
protected $policy;
|
||||||
|
protected $pool;
|
||||||
|
protected $rules;
|
||||||
|
protected $jobs;
|
||||||
|
protected $installedMap;
|
||||||
|
|
||||||
|
public function __construct(PolicyInterface $policy, Pool $pool)
|
||||||
|
{
|
||||||
|
$this->policy = $policy;
|
||||||
|
$this->pool = $pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new rule for the requirements of a package
|
||||||
|
*
|
||||||
|
* This rule is of the form (-A|B|C), where B and C are the providers of
|
||||||
|
* one requirement of the package A.
|
||||||
|
*
|
||||||
|
* @param PackageInterface $package The package with a requirement
|
||||||
|
* @param array $providers The providers of the requirement
|
||||||
|
* @param int $reason A RULE_* constant describing the
|
||||||
|
* reason for generating this rule
|
||||||
|
* @param mixed $reasonData Any data, e.g. the requirement name,
|
||||||
|
* that goes with the reason
|
||||||
|
* @return Rule The generated rule or null if tautological
|
||||||
|
*/
|
||||||
|
protected function createRequireRule(PackageInterface $package, array $providers, $reason, $reasonData = null)
|
||||||
|
{
|
||||||
|
$literals = array(-$package->getId());
|
||||||
|
|
||||||
|
foreach ($providers as $provider) {
|
||||||
|
// self fulfilling rule?
|
||||||
|
if ($provider === $package) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$literals[] = $provider->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Rule($this->pool, $literals, $reason, $reasonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a rule to install at least one of a set of packages
|
||||||
|
*
|
||||||
|
* The rule is (A|B|C) with A, B and C different packages. If the given
|
||||||
|
* set of packages is empty an impossible rule is generated.
|
||||||
|
*
|
||||||
|
* @param array $packages The set of packages to choose from
|
||||||
|
* @param int $reason A RULE_* constant describing the reason for
|
||||||
|
* generating this rule
|
||||||
|
* @param array $job The job this rule was created from
|
||||||
|
* @return Rule The generated rule
|
||||||
|
*/
|
||||||
|
protected function createInstallOneOfRule(array $packages, $reason, $job)
|
||||||
|
{
|
||||||
|
$literals = array();
|
||||||
|
foreach ($packages as $package) {
|
||||||
|
$literals[] = $package->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Rule($this->pool, $literals, $reason, $job['packageName'], $job);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a rule to remove a package
|
||||||
|
*
|
||||||
|
* The rule for a package A is (-A).
|
||||||
|
*
|
||||||
|
* @param PackageInterface $package The package to be removed
|
||||||
|
* @param int $reason A RULE_* constant describing the
|
||||||
|
* reason for generating this rule
|
||||||
|
* @param array $job The job this rule was created from
|
||||||
|
* @return Rule The generated rule
|
||||||
|
*/
|
||||||
|
protected function createRemoveRule(PackageInterface $package, $reason, $job)
|
||||||
|
{
|
||||||
|
return new Rule($this->pool, array(-$package->getId()), $reason, $job['packageName'], $job);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a rule for two conflicting packages
|
||||||
|
*
|
||||||
|
* The rule for conflicting packages A and B is (-A|-B). A is called the issuer
|
||||||
|
* and B the provider.
|
||||||
|
*
|
||||||
|
* @param PackageInterface $issuer The package declaring the conflict
|
||||||
|
* @param Package $provider The package causing the conflict
|
||||||
|
* @param int $reason A RULE_* constant describing the
|
||||||
|
* reason for generating this rule
|
||||||
|
* @param mixed $reasonData Any data, e.g. the package name, that
|
||||||
|
* goes with the reason
|
||||||
|
* @return Rule The generated rule
|
||||||
|
*/
|
||||||
|
protected function createConflictRule(PackageInterface $issuer, PackageInterface $provider, $reason, $reasonData = null)
|
||||||
|
{
|
||||||
|
// ignore self conflict
|
||||||
|
if ($issuer === $provider) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Rule($this->pool, array(-$issuer->getId(), -$provider->getId()), $reason, $reasonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a rule unless it duplicates an existing one of any type
|
||||||
|
*
|
||||||
|
* To be able to directly pass in the result of one of the rule creation
|
||||||
|
* methods.
|
||||||
|
*
|
||||||
|
* @param int $type A TYPE_* constant defining the rule type
|
||||||
|
* @param Rule $newRule The rule about to be added
|
||||||
|
*/
|
||||||
|
private function addRule($type, Rule $newRule = null) {
|
||||||
|
if ($this->rules->containsEqual($newRule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rules->add($newRule, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function addRulesForPackage(PackageInterface $package)
|
||||||
|
{
|
||||||
|
$workQueue = new \SplQueue;
|
||||||
|
$workQueue->enqueue($package);
|
||||||
|
|
||||||
|
while (!$workQueue->isEmpty()) {
|
||||||
|
$package = $workQueue->dequeue();
|
||||||
|
if (isset($this->addedMap[$package->getId()])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addedMap[$package->getId()] = true;
|
||||||
|
|
||||||
|
foreach ($package->getRequires() as $link) {
|
||||||
|
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
|
||||||
|
|
||||||
|
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, (string) $link));
|
||||||
|
|
||||||
|
foreach ($possibleRequires as $require) {
|
||||||
|
$workQueue->enqueue($require);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($package->getConflicts() as $link) {
|
||||||
|
$possibleConflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
|
||||||
|
|
||||||
|
foreach ($possibleConflicts as $conflict) {
|
||||||
|
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $conflict, Rule::RULE_PACKAGE_CONFLICT, (string) $link));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check obsoletes and implicit obsoletes of a package
|
||||||
|
$isInstalled = (isset($this->installedMap[$package->getId()]));
|
||||||
|
|
||||||
|
foreach ($package->getReplaces() as $link) {
|
||||||
|
$obsoleteProviders = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
|
||||||
|
|
||||||
|
foreach ($obsoleteProviders as $provider) {
|
||||||
|
if ($provider === $package) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->obsoleteImpossibleForAlias($package, $provider)) {
|
||||||
|
$reason = ($isInstalled) ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES;
|
||||||
|
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $provider, $reason, (string) $link));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check implicit obsoletes
|
||||||
|
// for installed packages we only need to check installed/installed problems,
|
||||||
|
// as the others are picked up when looking at the uninstalled package.
|
||||||
|
if (!$isInstalled) {
|
||||||
|
$obsoleteProviders = $this->pool->whatProvides($package->getName(), null);
|
||||||
|
|
||||||
|
foreach ($obsoleteProviders as $provider) {
|
||||||
|
if ($provider === $package) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
|
||||||
|
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, (string) $package));
|
||||||
|
} elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
|
||||||
|
$reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
|
||||||
|
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function obsoleteImpossibleForAlias($package, $provider)
|
||||||
|
{
|
||||||
|
$packageIsAlias = $package instanceof AliasPackage;
|
||||||
|
$providerIsAlias = $provider instanceof AliasPackage;
|
||||||
|
|
||||||
|
$impossible = (
|
||||||
|
($packageIsAlias && $package->getAliasOf() === $provider) ||
|
||||||
|
($providerIsAlias && $provider->getAliasOf() === $package) ||
|
||||||
|
($packageIsAlias && $providerIsAlias && $provider->getAliasOf() === $package->getAliasOf())
|
||||||
|
);
|
||||||
|
|
||||||
|
return $impossible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all rules for all update packages of a given package
|
||||||
|
*
|
||||||
|
* @param PackageInterface $package Rules for this package's updates are to
|
||||||
|
* be added
|
||||||
|
* @param bool $allowAll Whether downgrades are allowed
|
||||||
|
*/
|
||||||
|
private function addRulesForUpdatePackages(PackageInterface $package)
|
||||||
|
{
|
||||||
|
$updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package);
|
||||||
|
|
||||||
|
foreach ($updates as $update) {
|
||||||
|
$this->addRulesForPackage($update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function addRulesForJobs()
|
||||||
|
{
|
||||||
|
foreach ($this->jobs as $job) {
|
||||||
|
switch ($job['cmd']) {
|
||||||
|
case 'install':
|
||||||
|
if ($job['packages']) {
|
||||||
|
foreach ($job['packages'] as $package) {
|
||||||
|
if (!isset($this->installedMap[$package->getId()])) {
|
||||||
|
$this->addRulesForPackage($package);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rule = $this->createInstallOneOfRule($job['packages'], Rule::RULE_JOB_INSTALL, $job);
|
||||||
|
$this->addRule(RuleSet::TYPE_JOB, $rule);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
// remove all packages with this name including uninstalled
|
||||||
|
// ones to make sure none of them are picked as replacements
|
||||||
|
foreach ($job['packages'] as $package) {
|
||||||
|
$rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE, $job);
|
||||||
|
$this->addRule(RuleSet::TYPE_JOB, $rule);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRulesFor($jobs, $installedMap)
|
||||||
|
{
|
||||||
|
$this->jobs = $jobs;
|
||||||
|
$this->rules = new RuleSet;
|
||||||
|
$this->installedMap = $installedMap;
|
||||||
|
|
||||||
|
foreach ($this->installedMap as $package) {
|
||||||
|
$this->addRulesForPackage($package);
|
||||||
|
$this->addRulesForUpdatePackages($package);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addRulesForJobs();
|
||||||
|
|
||||||
|
return $this->rules;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extension of SplDoublyLinkedList with seek and removal of current element
|
||||||
|
*
|
||||||
|
* SplDoublyLinkedList only allows deleting a particular offset and has no
|
||||||
|
* method to set the internal iterator to a particular offset.
|
||||||
|
*
|
||||||
|
* @author Nils Adermann <naderman@naderman.de>
|
||||||
|
*/
|
||||||
|
class RuleWatchChain extends \SplDoublyLinkedList
|
||||||
|
{
|
||||||
|
protected $offset = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the internal iterator to the specified offset
|
||||||
|
*
|
||||||
|
* @param int $offset The offset to seek to.
|
||||||
|
*/
|
||||||
|
public function seek($offset)
|
||||||
|
{
|
||||||
|
$this->rewind();
|
||||||
|
for ($i = 0; $i < $offset; $i++, $this->next());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the current element from the list
|
||||||
|
*
|
||||||
|
* As SplDoublyLinkedList only allows deleting a particular offset and
|
||||||
|
* incorrectly sets the internal iterator if you delete the current value
|
||||||
|
* this method sets the internal iterator back to the following element
|
||||||
|
* using the seek method.
|
||||||
|
*/
|
||||||
|
public function remove()
|
||||||
|
{
|
||||||
|
$offset = $this->key();
|
||||||
|
$this->offsetUnset($offset);
|
||||||
|
$this->seek($offset);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RuleWatchGraph efficiently propagates decisions to other rules
|
||||||
|
*
|
||||||
|
* All rules generated for solving a SAT problem should be inserted into the
|
||||||
|
* graph. When a decision on a literal is made, the graph can be used to
|
||||||
|
* propagate the decision to all other rules involving the rule, leading to
|
||||||
|
* other trivial decisions resulting from unit clauses.
|
||||||
|
*
|
||||||
|
* @author Nils Adermann <naderman@naderman.de>
|
||||||
|
*/
|
||||||
|
class RuleWatchGraph
|
||||||
|
{
|
||||||
|
protected $watchChains = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a rule node into the appropriate chains within the graph
|
||||||
|
*
|
||||||
|
* The node is prepended to the watch chains for each of the two literals it
|
||||||
|
* watches.
|
||||||
|
*
|
||||||
|
* Assertions are skipped because they only depend on a single package and
|
||||||
|
* have no alternative literal that could be true, so there is no need to
|
||||||
|
* watch chnages in any literals.
|
||||||
|
*
|
||||||
|
* @param RuleWatchNode $node The rule node to be inserted into the graph
|
||||||
|
*/
|
||||||
|
public function insert(RuleWatchNode $node)
|
||||||
|
{
|
||||||
|
if ($node->getRule()->isAssertion()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array($node->watch1, $node->watch2) as $literal) {
|
||||||
|
if (!isset($this->watchChains[$literal])) {
|
||||||
|
$this->watchChains[$literal] = new RuleWatchChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->watchChains[$literal]->unshift($node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propagates a decision on a literal to all rules watching the literal
|
||||||
|
*
|
||||||
|
* If a decision, e.g. +A has been made, then all rules containing -A, e.g.
|
||||||
|
* (-A|+B|+C) now need to satisfy at least one of the other literals, so
|
||||||
|
* that the rule as a whole becomes true, since with +A applied the rule
|
||||||
|
* is now (false|+B|+C) so essentialy (+B|+C).
|
||||||
|
*
|
||||||
|
* This means that all rules watching the literal -A need to be updated to
|
||||||
|
* watch 2 other literals which can still be satisfied instead. So literals
|
||||||
|
* that conflict with previously made decisions are not an option.
|
||||||
|
*
|
||||||
|
* Alternatively it can occur that a unit clause results: e.g. if in the
|
||||||
|
* above example the rule was (-A|+B), then A turning true means that
|
||||||
|
* B must now be decided true as well.
|
||||||
|
*
|
||||||
|
* @param int $decidedLiteral The literal which was decided (A in our example)
|
||||||
|
* @param int $level The level at which the decision took place and at which
|
||||||
|
* all resulting decisions should be made.
|
||||||
|
* @param Callable $decisionsSatisfyCallback A callback which checks if a
|
||||||
|
* literal has already been positively decided and the rule is thus
|
||||||
|
* already true and can be skipped.
|
||||||
|
* @param Callable $conflictCallback A callback which checks if a literal
|
||||||
|
* would conflict with previously made decisions on the same package
|
||||||
|
* @param Callable $decideCallback A callback which is responsible for
|
||||||
|
* registering decided literals resulting from unit clauses
|
||||||
|
* @return Rule|null If a conflict is found the conflicting rule is returned
|
||||||
|
*/
|
||||||
|
public function propagateLiteral($decidedLiteral, $level, $decisionsSatisfyCallback, $conflictCallback, $decideCallback)
|
||||||
|
{
|
||||||
|
// we invert the decided literal here, example:
|
||||||
|
// A was decided => (-A|B) now requires B to be true, so we look for
|
||||||
|
// rules which are fulfilled by -A, rather than A.
|
||||||
|
$literal = -$decidedLiteral;
|
||||||
|
|
||||||
|
if (!isset($this->watchChains[$literal])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chain = $this->watchChains[$literal];
|
||||||
|
|
||||||
|
$chain->rewind();
|
||||||
|
while ($chain->valid()) {
|
||||||
|
$node = $chain->current();
|
||||||
|
$otherWatch = $node->getOtherWatch($literal);
|
||||||
|
|
||||||
|
if (!$node->getRule()->isDisabled() && !call_user_func($decisionsSatisfyCallback, $otherWatch)) {
|
||||||
|
$ruleLiterals = $node->getRule()->getLiterals();
|
||||||
|
|
||||||
|
$alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $conflictCallback) {
|
||||||
|
return $literal !== $ruleLiteral &&
|
||||||
|
$otherWatch !== $ruleLiteral &&
|
||||||
|
!call_user_func($conflictCallback, $ruleLiteral);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($alternativeLiterals) {
|
||||||
|
reset($alternativeLiterals);
|
||||||
|
$this->moveWatch($literal, current($alternativeLiterals), $node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call_user_func($conflictCallback, $otherWatch)) {
|
||||||
|
return $node->getRule();
|
||||||
|
}
|
||||||
|
|
||||||
|
call_user_func($decideCallback, $otherWatch, $level, $node->getRule());
|
||||||
|
}
|
||||||
|
|
||||||
|
$chain->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a rule node from one watch chain to another
|
||||||
|
*
|
||||||
|
* The rule node's watched literals are updated accordingly.
|
||||||
|
*
|
||||||
|
* @param $fromLiteral A literal the node used to watch
|
||||||
|
* @param $toLiteral A literal the node should watch now
|
||||||
|
* @param $node The rule node to be moved
|
||||||
|
*/
|
||||||
|
protected function moveWatch($fromLiteral, $toLiteral, $node)
|
||||||
|
{
|
||||||
|
if (!isset($this->watchChains[$toLiteral])) {
|
||||||
|
$this->watchChains[$toLiteral] = new RuleWatchChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node->moveWatch($fromLiteral, $toLiteral);
|
||||||
|
$this->watchChains[$fromLiteral]->remove();
|
||||||
|
$this->watchChains[$toLiteral]->unshift($node);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around a Rule which keeps track of the two literals it watches
|
||||||
|
*
|
||||||
|
* Used by RuleWatchGraph to store rules in two RuleWatchChains.
|
||||||
|
*
|
||||||
|
* @author Nils Adermann <naderman@naderman.de>
|
||||||
|
*/
|
||||||
|
class RuleWatchNode
|
||||||
|
{
|
||||||
|
public $watch1;
|
||||||
|
public $watch2;
|
||||||
|
|
||||||
|
protected $rule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new node watching the first and second literals of the rule.
|
||||||
|
*
|
||||||
|
* @param Rule $rule The rule to wrap
|
||||||
|
*/
|
||||||
|
public function __construct($rule)
|
||||||
|
{
|
||||||
|
$this->rule = $rule;
|
||||||
|
|
||||||
|
$literals = $rule->getLiterals();
|
||||||
|
|
||||||
|
$this->watch1 = count($literals) > 0 ? $literals[0] : 0;
|
||||||
|
$this->watch2 = count($literals) > 1 ? $literals[1] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places the second watch on the rule's literal, decided at the highest level
|
||||||
|
*
|
||||||
|
* Useful for learned rules where the literal for the highest rule is most
|
||||||
|
* likely to quickly lead to further decisions.
|
||||||
|
*
|
||||||
|
* @param SplFixedArray $decisionMap A package to decision lookup table
|
||||||
|
*/
|
||||||
|
public function watch2OnHighest($decisionMap)
|
||||||
|
{
|
||||||
|
$literals = $this->rule->getLiterals();
|
||||||
|
|
||||||
|
// if there are only 2 elements, both are being watched anyway
|
||||||
|
if ($literals < 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$watchLevel = 0;
|
||||||
|
|
||||||
|
foreach ($literals as $literal) {
|
||||||
|
$level = abs($decisionMap[abs($literal)]);
|
||||||
|
|
||||||
|
if ($level > $watchLevel) {
|
||||||
|
$this->rule->watch2 = $literal;
|
||||||
|
$watchLevel = $level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the rule this node wraps
|
||||||
|
*
|
||||||
|
* @return Rule
|
||||||
|
*/
|
||||||
|
public function getRule()
|
||||||
|
{
|
||||||
|
return $this->rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given one watched literal, this method returns the other watched literal
|
||||||
|
*
|
||||||
|
* @param int The watched literal that should not be returned
|
||||||
|
* @return int A literal
|
||||||
|
*/
|
||||||
|
public function getOtherWatch($literal)
|
||||||
|
{
|
||||||
|
if ($this->watch1 == $literal) {
|
||||||
|
return $this->watch2;
|
||||||
|
} else {
|
||||||
|
return $this->watch1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a watch from one literal to another
|
||||||
|
*
|
||||||
|
* @param int $from The previously watched literal
|
||||||
|
* @param int $to The literal to be watched now
|
||||||
|
*/
|
||||||
|
public function moveWatch($from, $to)
|
||||||
|
{
|
||||||
|
if ($this->watch1 == $from) {
|
||||||
|
$this->watch1 = $to;
|
||||||
|
} else {
|
||||||
|
$this->watch2 = $to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,167 @@
|
|||||||
|
<?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\Package\AliasPackage;
|
||||||
|
use Composer\DependencyResolver\Operation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nils Adermann <naderman@naderman.de>
|
||||||
|
*/
|
||||||
|
class Transaction
|
||||||
|
{
|
||||||
|
protected $policy;
|
||||||
|
protected $pool;
|
||||||
|
protected $installedMap;
|
||||||
|
protected $decisionMap;
|
||||||
|
protected $decisionQueue;
|
||||||
|
protected $decisionQueueWhy;
|
||||||
|
|
||||||
|
public function __construct($policy, $pool, $installedMap, $decisionMap, array $decisionQueue, $decisionQueueWhy)
|
||||||
|
{
|
||||||
|
$this->policy = $policy;
|
||||||
|
$this->pool = $pool;
|
||||||
|
$this->installedMap = $installedMap;
|
||||||
|
$this->decisionMap = $decisionMap;
|
||||||
|
$this->decisionQueue = $decisionQueue;
|
||||||
|
$this->decisionQueueWhy = $decisionQueueWhy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOperations()
|
||||||
|
{
|
||||||
|
$transaction = array();
|
||||||
|
$installMeansUpdateMap = array();
|
||||||
|
|
||||||
|
foreach ($this->decisionQueue as $i => $literal) {
|
||||||
|
$package = $this->pool->literalToPackage($literal);
|
||||||
|
|
||||||
|
// !wanted & installed
|
||||||
|
if ($literal <= 0 && isset($this->installedMap[$package->getId()])) {
|
||||||
|
$updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package);
|
||||||
|
|
||||||
|
$literals = array($package->getId());
|
||||||
|
|
||||||
|
foreach ($updates as $update) {
|
||||||
|
$literals[] = $update->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($literals as $updateLiteral) {
|
||||||
|
if ($updateLiteral !== $literal) {
|
||||||
|
$installMeansUpdateMap[abs($updateLiteral)] = $package;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->decisionQueue as $i => $literal) {
|
||||||
|
$package = $this->pool->literalToPackage($literal);
|
||||||
|
|
||||||
|
// wanted & installed || !wanted & !installed
|
||||||
|
if (($literal > 0) == (isset($this->installedMap[$package->getId()]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($literal > 0) {
|
||||||
|
if ($package instanceof AliasPackage) {
|
||||||
|
$transaction[] = new Operation\MarkAliasInstalledOperation(
|
||||||
|
$package, $this->decisionQueueWhy[$i]
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($installMeansUpdateMap[abs($literal)])) {
|
||||||
|
|
||||||
|
$source = $installMeansUpdateMap[abs($literal)];
|
||||||
|
|
||||||
|
$transaction[] = new Operation\UpdateOperation(
|
||||||
|
$source, $package, $this->decisionQueueWhy[$i]
|
||||||
|
);
|
||||||
|
|
||||||
|
// avoid updates to one package from multiple origins
|
||||||
|
unset($installMeansUpdateMap[abs($literal)]);
|
||||||
|
$ignoreRemove[$source->getId()] = true;
|
||||||
|
} else {
|
||||||
|
$transaction[] = new Operation\InstallOperation(
|
||||||
|
$package, $this->decisionQueueWhy[$i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (!isset($ignoreRemove[$package->getId()])) {
|
||||||
|
if ($package instanceof AliasPackage) {
|
||||||
|
$transaction[] = new Operation\MarkAliasInstalledOperation(
|
||||||
|
$package, $this->decisionQueueWhy[$i]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$transaction[] = new Operation\UninstallOperation(
|
||||||
|
$package, $this->decisionQueueWhy[$i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$allDecidedMap = $this->decisionMap;
|
||||||
|
foreach ($this->decisionMap as $packageId => $decision) {
|
||||||
|
if ($decision != 0) {
|
||||||
|
$package = $this->pool->packageById($packageId);
|
||||||
|
if ($package instanceof AliasPackage) {
|
||||||
|
$allDecidedMap[$package->getAliasOf()->getId()] = $decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($allDecidedMap as $packageId => $decision) {
|
||||||
|
if ($packageId === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 == $decision && isset($this->installedMap[$packageId])) {
|
||||||
|
$package = $this->pool->packageById($packageId);
|
||||||
|
|
||||||
|
if ($package instanceof AliasPackage) {
|
||||||
|
$transaction[] = new Operation\MarkAliasInstalledOperation(
|
||||||
|
$package, null
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$transaction[] = new Operation\UninstallOperation(
|
||||||
|
$package, null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->decisionMap[$packageId] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($allDecidedMap as $packageId => $decision) {
|
||||||
|
if ($packageId === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 == $decision && isset($this->installedMap[$packageId])) {
|
||||||
|
$package = $this->pool->packageById($packageId);
|
||||||
|
|
||||||
|
if ($package instanceof AliasPackage) {
|
||||||
|
$transaction[] = new Operation\MarkAliasInstalledOperation(
|
||||||
|
$package, null
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$transaction[] = new Operation\UninstallOperation(
|
||||||
|
$package, null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->decisionMap[$packageId] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_reverse($transaction);
|
||||||
|
}
|
||||||
|
}
|
@ -1,63 +0,0 @@
|
|||||||
<?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\Test\DependencyResolver;
|
|
||||||
|
|
||||||
use Composer\DependencyResolver\Literal;
|
|
||||||
use Composer\Test\TestCase;
|
|
||||||
|
|
||||||
class LiteralTest extends TestCase
|
|
||||||
{
|
|
||||||
protected $package;
|
|
||||||
|
|
||||||
public function setUp()
|
|
||||||
{
|
|
||||||
$this->package = $this->getPackage('foo', '1');
|
|
||||||
$this->package->setId(12);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testLiteralWanted()
|
|
||||||
{
|
|
||||||
$literal = new Literal($this->package, true);
|
|
||||||
|
|
||||||
$this->assertEquals(12, $literal->getId());
|
|
||||||
$this->assertEquals('+'.(string) $this->package, (string) $literal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testLiteralUnwanted()
|
|
||||||
{
|
|
||||||
$literal = new Literal($this->package, false);
|
|
||||||
|
|
||||||
$this->assertEquals(-12, $literal->getId());
|
|
||||||
$this->assertEquals('-'.(string) $this->package, (string) $literal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testLiteralInverted()
|
|
||||||
{
|
|
||||||
$literal = new Literal($this->package, false);
|
|
||||||
|
|
||||||
$inverted = $literal->inverted();
|
|
||||||
|
|
||||||
$this->assertInstanceOf('\Composer\DependencyResolver\Literal', $inverted);
|
|
||||||
$this->assertTrue($inverted->isWanted());
|
|
||||||
$this->assertSame($this->package, $inverted->getPackage());
|
|
||||||
$this->assertFalse($literal->equals($inverted));
|
|
||||||
|
|
||||||
$doubleInverted = $inverted->inverted();
|
|
||||||
|
|
||||||
$this->assertInstanceOf('\Composer\DependencyResolver\Literal', $doubleInverted);
|
|
||||||
$this->assertFalse($doubleInverted->isWanted());
|
|
||||||
$this->assertSame($this->package, $doubleInverted->getPackage());
|
|
||||||
|
|
||||||
$this->assertTrue($literal->equals($doubleInverted));
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue