* Jordi Boggiano * * 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 */ 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(new Literal($package, false)); foreach ($providers as $provider) { // self fulfilling rule? if ($provider === $package) { return null; } $literals[] = new Literal($provider, true); } return new Rule($literals, $reason, $reasonData); } /** * Creates a new rule for installing a package * * The rule is simply (A) for a package A to be installed. * * @param PackageInterface $package The package to be installed * @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 createInstallRule(PackageInterface $package, $reason, $reasonData = null) { return new Rule(new Literal($package, true)); } /** * 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[] = new Literal($package, true); } return new Rule($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(array(new Literal($package, false)), $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(array(new Literal($issuer, false), new Literal($provider, false)), $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)); } else if (!$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; } }