You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

456 lines
19 KiB
PHTML

<?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\IO\IOInterface;
use Composer\Package\AliasPackage;
use Composer\Package\BasePackage;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Package\Version\StabilityFilter;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RootPackageRepository;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Plugin\PrePoolCreateEvent;
use Composer\Plugin\PluginEvents;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
class PoolBuilder
{
/**
* @var int[]
*/
private $acceptableStabilities;
/**
* @var int[]
*/
private $stabilityFlags;
/**
* @psalm-var array<string, array<string, array{alias: string, alias_normalized: string}>>
*/
private $rootAliases;
/**
* @psalm-var array<string, string>
*/
private $rootReferences;
/**
* @var EventDispatcher
*/
private $eventDispatcher;
/**
* @var IOInterface
*/
private $io;
/**
* @psalm-var array<string, AliasPackage>
*/
private $aliasMap = array();
/**
* @psalm-var array<string, ConstraintInterface[]>
*/
private $packagesToLoad = array();
/**
* @psalm-var array<string, ConstraintInterface[]>
*/
private $loadedNames = array();
/**
* @psalm-var Package[]
*/
private $packages = array();
/**
* @psalm-var list<Package>
*/
private $unacceptableFixedPackages = array();
private $updateAllowList = array();
private $skippedLoad = array();
/**
* @psalm-var array<string, bool>
*/
private $updateAllowWarned = array();
/**
* @param int[] $acceptableStabilities array of stability => BasePackage::STABILITY_* value
* @psalm-param array<string, int> $acceptableStabilities
* @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value
* @psalm-param array<string, int> $stabilityFlags
* @param array[] $rootAliases
* @psalm-param list<array{package: string, version: string, alias: string, alias_normalized: string}> $rootAliases
* @param string[] $rootReferences an array of package name => source reference
* @psalm-param array<string, string> $rootReferences
*/
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null)
{
$this->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags;
$this->rootAliases = $this->getRootAliasesPerPackage($rootAliases);
$this->rootReferences = $rootReferences;
$this->eventDispatcher = $eventDispatcher;
$this->io = $io;
}
public function buildPool(array $repositories, Request $request)
{
if ($request->getUpdateAllowList()) {
$this->updateAllowList = $request->getUpdateAllowList();
$this->warnAboutNonMatchingUpdateAllowList($request);
foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) {
if (!$this->isUpdateAllowed($lockedPackage)) {
$request->fixPackage($lockedPackage);
$lockedName = $lockedPackage->getName();
// remember which packages we skipped loading remote content for in this partial update
$this->skippedLoad[$lockedName] = $lockedName;
foreach ($lockedPackage->getReplaces() as $link) {
$this->skippedLoad[$link->getTarget()] = $lockedName;
}
}
}
}
foreach ($request->getFixedPackages() as $package) {
$this->loadedNames[$package->getName()] = new EmptyConstraint();
// replace means conflict, so if a fixed package replaces a name, no need to load that one, packages would conflict anyways
foreach ($package->getReplaces() as $link) {
$this->loadedNames[$link->getTarget()] = new EmptyConstraint();
}
// TODO in how far can we do the above for conflicts? It's more tricky cause conflicts can be limited to
// specific versions while replace is a conflict with all versions of the name
if (
$package->getRepository() instanceof RootPackageRepository
|| $package->getRepository() instanceof PlatformRepository
|| StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability())
) {
$this->loadPackage($request, $package, false);
} else {
$this->unacceptableFixedPackages[] = $package;
}
}
foreach ($request->getRequires() as $packageName => $constraint) {
$this->markPackageNameForLoading($packageName, $constraint);
}
// clean up packagesToLoad for anything we manually marked loaded above
foreach ($this->packagesToLoad as $name => $constraint) {
if (isset($this->loadedNames[$name])) {
unset($this->packagesToLoad[$name]);
}
}
while (!empty($this->packagesToLoad)) {
$this->loadPackagesMarkedForLoading($request, $repositories);
}
foreach ($this->packages as $i => $package) {
// we check all alias related packages at once, so no need to check individual aliases
// isset also checks non-null value
if (!$package instanceof AliasPackage) {
$constraint = new Constraint('==', $package->getVersion());
$aliasedPackages = array($i => $package);
if (isset($this->aliasMap[spl_object_hash($package)])) {
$aliasedPackages += $this->aliasMap[spl_object_hash($package)];
}
$found = false;
foreach ($aliasedPackages as $packageOrAlias) {
if ($constraint->matches(new Constraint('==', $packageOrAlias->getVersion()))) {
$found = true;
}
}
if (!$found) {
foreach ($aliasedPackages as $index => $packageOrAlias) {
unset($this->packages[$index]);
}
}
}
}
if ($this->eventDispatcher) {
$prePoolCreateEvent = new PrePoolCreateEvent(
PluginEvents::PRE_POOL_CREATE,
$repositories,
$request,
$this->acceptableStabilities,
$this->stabilityFlags,
$this->rootAliases,
$this->rootReferences,
$this->packages,
$this->unacceptableFixedPackages
);
$this->eventDispatcher->dispatch($prePoolCreateEvent->getName(), $prePoolCreateEvent);
$this->packages = $prePoolCreateEvent->getPackages();
$this->unacceptableFixedPackages = $prePoolCreateEvent->getUnacceptableFixedPackages();
}
$pool = new Pool($this->packages, $this->unacceptableFixedPackages);
$this->aliasMap = array();
$this->packagesToLoad = array();
$this->loadedNames = array();
$this->packages = array();
$this->unacceptableFixedPackages = array();
return $pool;
}
private function markPackageNameForLoading($name, ConstraintInterface $constraint)
{
if (!isset($this->loadedNames[$name])) {
$this->packagesToLoad[$name] = $constraint;
return;
}
// No need to load this package with this constraint because it was
// already loaded in one that matches
if ($this->loadedNames[$name]->matches($constraint)) {
return;
}
// Maybe it was already marked before but not loaded yet. In that case
// we have to extend the constraint (we don't check if the match because
// MultiConstraint::create() will optimize anyway
if (isset($this->packagesToLoad[$name])) {
$constraint = MultiConstraint::create(array($this->packagesToLoad[$name], $constraint), false);
}
// We have already loaded that package but not in the constraint that's
// required. We extend the constraint and mark that package as not being loaded
// yet so we get the required package versions
$this->packagesToLoad[$name] = MultiConstraint::create(array($this->loadedNames[$name], $constraint), false);
unset($this->loadedNames[$name]);
}
private function loadPackagesMarkedForLoading(Request $request, $repositories)
{
foreach ($this->packagesToLoad as $name => $constraint) {
$this->loadedNames[$name] = $constraint;
}
foreach ($repositories as $repository) {
if (empty($this->packagesToLoad)) {
break;
}
// these repos have their packages fixed if they need to be loaded so we
// never need to load anything else from them
if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) {
continue;
}
$result = $repository->loadPackages($this->packagesToLoad, $this->acceptableStabilities, $this->stabilityFlags);
foreach ($result['namesFound'] as $name) {
// avoid loading the same package again from other repositories once it has been found
unset($this->packagesToLoad[$name]);
}
foreach ($result['packages'] as $package) {
$this->loadPackage($request, $package);
}
}
// Make sure we empty the packagesToLoad here as it would result
// in an endless loop with non-existent packages for example
// TODO: fixme, this should only happen if it's not a new package
// $this->packagesToLoad = array();
}
private function loadPackage(Request $request, PackageInterface $package, $propagateUpdate = true)
{
end($this->packages);
$index = key($this->packages) + 1;
$this->packages[] = $package;
if ($package instanceof AliasPackage) {
$this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package;
}
$name = $package->getName();
// we're simply setting the root references on all versions for a name here and rely on the solver to pick the
// right version. It'd be more work to figure out which versions and which aliases of those versions this may
// apply to
if (isset($this->rootReferences[$name])) {
// do not modify the references on already locked packages
if (!$request->isFixedPackage($package)) {
$package->setSourceDistReferences($this->rootReferences[$name]);
}
}
// if propogateUpdate is false we are loading a fixed package, root aliases do not apply as they are manually
// loaded as separate packages in this case
if ($propagateUpdate && isset($this->rootAliases[$name][$package->getVersion()])) {
$alias = $this->rootAliases[$name][$package->getVersion()];
if ($package instanceof AliasPackage) {
$basePackage = $package->getAliasOf();
} else {
$basePackage = $package;
}
$aliasPackage = new AliasPackage($basePackage, $alias['alias_normalized'], $alias['alias']);
$aliasPackage->setRootPackageAlias(true);
$this->packages[] = $aliasPackage;
$this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$index+1] = $aliasPackage;
}
foreach ($package->getRequires() as $link) {
$require = $link->getTarget();
4 years ago
$linkConstraint = $link->getConstraint();
// if this is a partial update with transitive dependencies we need to unfix the package we now know is a
// dependency of another package which we are trying to update, and then attempt to load it again
if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies() && isset($this->skippedLoad[$require])) {
if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$require])) {
$this->unfixPackage($request, $require);
$this->markPackageNameForLoading($require, $linkConstraint);
} elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $require) && !isset($this->updateAllowWarned[$require])) {
$this->updateAllowWarned[$require] = true;
$this->io->writeError('<warning>Dependency "'.$require.'" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies.</warning>');
}
}
$this->markPackageNameForLoading($require, $linkConstraint);
}
// if we're doing a partial update with deps we also need to unfix packages which are being replaced in case they
// are currently locked and thus prevent this updateable package from being installable/updateable
if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) {
foreach ($package->getReplaces() as $link) {
$replace = $link->getTarget();
if (isset($this->loadedNames[$replace]) && isset($this->skippedLoad[$replace])) {
if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$replace])) {
$this->unfixPackage($request, $replace);
$this->markPackageNameForLoading($replace, $link->getConstraint());
} elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$replace])) {
$this->updateAllowWarned[$replace] = true;
$this->io->writeError('<warning>Dependency "'.$replace.'" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies.</warning>');
}
}
}
}
}
/**
* Checks if a particular name is required directly in the request
*
* @return bool
*/
private function isRootRequire(Request $request, $name)
{
$rootRequires = $request->getRequires();
return isset($rootRequires[$name]);
}
/**
* Checks whether the update allow list allows this package in the lock file to be updated
* @return bool
*/
private function isUpdateAllowed(PackageInterface $package)
{
foreach ($this->updateAllowList as $pattern => $void) {
$patternRegexp = BasePackage::packageNameToRegexp($pattern);
if (preg_match($patternRegexp, $package->getName())) {
return true;
}
}
return false;
}
private function warnAboutNonMatchingUpdateAllowList(Request $request)
{
foreach ($this->updateAllowList as $pattern => $void) {
$patternRegexp = BasePackage::packageNameToRegexp($pattern);
// update pattern matches a locked package? => all good
foreach ($request->getLockedRepository()->getPackages() as $package) {
if (preg_match($patternRegexp, $package->getName())) {
continue 2;
}
}
// update pattern matches a root require? => all good, probably a new package
foreach ($request->getRequires() as $packageName => $constraint) {
if (preg_match($patternRegexp, $packageName)) {
continue 2;
}
}
if (strpos($pattern, '*') !== false) {
$this->io->writeError('<warning>Pattern "' . $pattern . '" listed for update does not match any locked packages.</warning>');
} else {
$this->io->writeError('<warning>Package "' . $pattern . '" listed for update is not locked.</warning>');
}
}
}
/**
* Reverts the decision to use a fixed package from lock file if a partial update with transitive dependencies
* found that this package actually needs to be updated
*/
private function unfixPackage(Request $request, $name)
{
// remove locked package by this name which was already initialized
foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) {
if (!($lockedPackage instanceof AliasPackage) && $lockedPackage->getName() === $name) {
if (false !== $index = array_search($lockedPackage, $this->packages, true)) {
$request->unfixPackage($lockedPackage);
unset($this->packages[$index]);
if (isset($this->aliasMap[spl_object_hash($lockedPackage)])) {
foreach ($this->aliasMap[spl_object_hash($lockedPackage)] as $aliasIndex => $aliasPackage) {
$request->unfixPackage($aliasPackage);
unset($this->packages[$aliasIndex]);
}
unset($this->aliasMap[spl_object_hash($lockedPackage)]);
}
}
}
}
if (
// if we unfixed a replaced package name, we also need to unfix the replacer itself
$this->skippedLoad[$name] !== $name
// as long as it was not unfixed yet
&& isset($this->skippedLoad[$this->skippedLoad[$name]])
) {
$this->unfixPackage($request, $this->skippedLoad[$name]);
}
unset($this->skippedLoad[$name]);
unset($this->loadedNames[$name]);
}
private function getRootAliasesPerPackage(array $aliases)
{
$normalizedAliases = array();
foreach ($aliases as $alias) {
$normalizedAliases[$alias['package']][$alias['version']] = array(
'alias' => $alias['alias'],
'alias_normalized' => $alias['alias_normalized'],
);
}
return $normalizedAliases;
}
}