|
@@ -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;
|
|
|
+ }
|
|
|
+}
|