@ -12,6 +12,7 @@
namespace Composer\DependencyResolver;
namespace Composer\DependencyResolver;
use Composer\IO\IOInterface;
use Composer\Package\AliasPackage;
use Composer\Package\AliasPackage;
use Composer\Package\BasePackage;
use Composer\Package\BasePackage;
use Composer\Package\Package;
use Composer\Package\Package;
@ -36,32 +37,41 @@ class PoolBuilder
private $rootAliases;
private $rootAliases;
private $rootReferences;
private $rootReferences;
private $eventDispatcher;
private $eventDispatcher;
private $io;
private $aliasMap = array();
private $aliasMap = array();
private $nameConstraints = array();
private $nameConstraints = array();
private $loadedNames = array();
private $loadedNames = array();
private $packages = array();
private $packages = array();
private $unacceptableFixedPackages = array();
private $unacceptableFixedPackages = array();
private $updateAllowList = array();
private $skippedLoad = array();
private $updateAllowWarned = array();
private $unfixList = array();
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, EventDispatcher $eventDispatcher = null, IOInterface $io = null)
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, EventDispatcher $eventDispatcher = null)
{
{
$this->acceptableStabilities = $acceptableStabilities;
$this->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags;
$this->stabilityFlags = $stabilityFlags;
$this->rootAliases = $rootAliases;
$this->rootAliases = $rootAliases;
$this->rootReferences = $rootReferences;
$this->rootReferences = $rootReferences;
$this->eventDispatcher = $eventDispatcher;
$this->eventDispatcher = $eventDispatcher;
$this->io = $io;
}
}
public function buildPool(array $repositories, Request $request)
public function buildPool(array $repositories, Request $request)
{
{
if ($request->getUpdateAllowList()) {
if ($request->getUpdateAllowList()) {
$this->unfixList = $request->getUpdateAllowList();
$this->updateAllowList = $request->getUpdateAllowList();
$this->warnAboutNonMatchingUpdateAllowList($request);
foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) {
foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) {
if (!$this->isUpdateable ($lockedPackage)) {
if (!$this->isUpdateAllowed ($lockedPackage)) {
$request->fixPackage($lockedPackage);
$request->fixPackage($lockedPackage);
// remember which packages we skipped loading remote content for in this partial update
$this->skippedLoad[$lockedPackage->getName()] = true;
foreach ($lockedPackage->getReplaces() as $link) {
$this->skippedLoad[$link->getTarget()] = true;
}
}
}
}
}
}
}
@ -85,7 +95,7 @@ class PoolBuilder
|| $package->getRepository() instanceof PlatformRepository
|| $package->getRepository() instanceof PlatformRepository
|| StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability())
|| StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability())
) {
) {
$loadNames += $this->loadPackage($request, $package);
$loadNames += $this->loadPackage($request, $package, false );
} else {
} else {
$this->unacceptableFixedPackages[] = $package;
$this->unacceptableFixedPackages[] = $package;
}
}
@ -94,6 +104,7 @@ class PoolBuilder
foreach ($request->getRequires() as $packageName => $constraint) {
foreach ($request->getRequires() as $packageName => $constraint) {
// fixed packages have already been added, so if a root require needs one of them, no need to do anything
// fixed packages have already been added, so if a root require needs one of them, no need to do anything
if (isset($this->loadedNames[$packageName])) {
if (isset($this->loadedNames[$packageName])) {
$this->rootRequireNotUpdated[$packageName] = true;
continue;
continue;
}
}
@ -120,7 +131,6 @@ class PoolBuilder
if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) {
if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) {
continue;
continue;
}
}
$result = $repository->loadPackages($loadNames, $this->acceptableStabilities, $this->stabilityFlags);
$result = $repository->loadPackages($loadNames, $this->acceptableStabilities, $this->stabilityFlags);
foreach ($result['namesFound'] as $name) {
foreach ($result['namesFound'] as $name) {
@ -189,7 +199,7 @@ class PoolBuilder
return $pool;
return $pool;
}
}
private function loadPackage(Request $request, PackageInterface $package)
private function loadPackage(Request $request, PackageInterface $package, $propagateUpdate = true )
{
{
$index = count($this->packages);
$index = count($this->packages);
$this->packages[] = $package;
$this->packages[] = $package;
@ -205,6 +215,7 @@ class PoolBuilder
// apply to
// apply to
if (isset($this->rootReferences[$name])) {
if (isset($this->rootReferences[$name])) {
// do not modify the references on already locked packages
// do not modify the references on already locked packages
// TODO what about unfix on allow update?
if (!$request->isFixedPackage($package)) {
if (!$request->isFixedPackage($package)) {
$package->setSourceDistReferences($this->rootReferences[$name]);
$package->setSourceDistReferences($this->rootReferences[$name]);
}
}
@ -229,9 +240,16 @@ class PoolBuilder
$require = $link->getTarget();
$require = $link->getTarget();
if (!isset($this->loadedNames[$require])) {
if (!isset($this->loadedNames[$require])) {
$loadNames[$require] = null;
$loadNames[$require] = null;
// 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
} elseif ($propagateUpdate & & $request->getUpdateAllowTransitiveDependencies() & & isset($this->skippedLoad[$require])) {
if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $require)) {
$this->unfixPackage($request, $require);
$loadNames[$require] = null;
} elseif (!$request->getUpdateAllowTransitiveRootDependencies() & & $this->isRootRequire($request, $require) & & !isset($this->updateAllowWarned[$require]) & & $this->io) {
$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 > ');
}
}
if (isset($request->getUpdateAllowList()[$package->getName()])) {
}
}
$linkConstraint = $link->getConstraint();
$linkConstraint = $link->getConstraint();
@ -252,121 +270,67 @@ class PoolBuilder
}
}
/**
/**
* Adds all dependencies of the update whitelist to the whitelist, too.
* Checks if a particular name is required directly in the request
*
* Packages which are listed as requirements in the root package will be
* skipped including their dependencies, unless they are listed in the
* update whitelist themselves or $whitelistAllDependencies is true.
*
*
* @param RepositoryInterface $lockRepo Use the locked repo
* @return bool
* As we want the most accurate package list to work with, and installed
* repo might be empty but locked repo will always be current.
* @param array $rootRequires An array of links to packages in require of the root package
* @param array $rootDevRequires An array of links to packages in require-dev of the root package
*/
*/
private function whitelistUpdateDependencies($lockRepo, array $rootRequires, array $rootDevRequires )
private function isRootRequire(Request $request, $name)
{
{
$rootRequires = array_merge($rootRequires, $rootDevRequires);
$rootRequires = $request->getRequires();
return isset($rootRequires[$name]);
$skipPackages = array();
if (!$this->whitelistAllDependencies) {
foreach ($rootRequires as $require) {
$skipPackages[$require->getTarget()] = true;
}
}
$installedRepo = new InstalledRepository(array($lockRepo));
$seen = array();
$rootRequiredPackageNames = array_keys($rootRequires);
foreach ($this->updateWhitelist as $packageName => $void) {
$packageQueue = new \SplQueue;
$nameMatchesRequiredPackage = false;
$depPackages = $installedRepo->findPackagesWithReplacersAndProviders($packageName);
$matchesByPattern = array();
// check if the name is a glob pattern that did not match directly
if (empty($depPackages)) {
// add any installed package matching the whitelisted name/pattern
$whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$');
foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) {
$matchesByPattern[] = $installedRepo->findPackages($installedPackage['name']);
}
// add root requirements which match the whitelisted name/pattern
$whitelistPatternRegexp = BasePackage::packageNameToRegexp($packageName);
foreach ($rootRequiredPackageNames as $rootRequiredPackageName) {
if (preg_match($whitelistPatternRegexp, $rootRequiredPackageName)) {
$nameMatchesRequiredPackage = true;
break;
}
}
}
if (!empty($matchesByPattern)) {
$depPackages = array_merge($depPackages, call_user_func_array('array_merge', $matchesByPattern));
}
if (count($depPackages) == 0 & & !$nameMatchesRequiredPackage) {
$this->io->writeError('< warning > Package "' . $packageName . '" listed for update is not installed. Ignoring.< / warning > ');
}
foreach ($depPackages as $depPackage) {
$packageQueue->enqueue($depPackage);
}
}
while (!$packageQueue->isEmpty()) {
/**
$package = $packageQueue->dequeue();
* Checks whether the update allow list allows this package in the lock file to be updated
if (isset($seen[spl_object_hash($package)])) {
* @return bool
continue;
*/
private function isUpdateAllowed(PackageInterface $package)
{
foreach ($this->updateAllowList as $pattern => $void) {
$patternRegexp = BasePackage::packageNameToRegexp($pattern);
if (preg_match($patternRegexp, $package->getName())) {
return true;
}
}
$seen[spl_object_hash($package)] = true;
$this->updateWhitelist[$package->getName()] = true;
if (!$this->whitelistTransitiveDependencies & & !$this->whitelistAllDependencies) {
continue;
}
}
$requires = $package->getRequires();
return false;
foreach ($requires as $require) {
$requirePackages = $installedRepo->findPackagesWithReplacersAndProviders($require->getTarget());
foreach ($requirePackages as $requirePackage) {
if (isset($this->updateWhitelist[$requirePackage->getName()])) {
continue;
}
}
if (isset($skipPackages[$requirePackage->getName()]) & & !preg_match(BasePackage::packageNameToRegexp($packageName), $requirePackage->getName())) {
private function warnAboutNonMatchingUpdateAllowList(Request $request)
$this->io->writeError('< warning > Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.< / warning > ');
{
continue;
if ($this->io) {
foreach ($this->updateAllowList as $pattern => $void) {
foreach ($request->getLockedRepository()->getPackages() as $package) {
$patternRegexp = BasePackage::packageNameToRegexp($pattern);
if (preg_match($patternRegexp, $package->getName())) {
continue 2;
}
}
$packageQueue->enqueue($requirePackage);
}
}
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 > ');
}
}
}
}
}
}
}
}
/**
/**
* @param PackageInterface $package
* Reverts the decision to use a fixed package from lock file if a partial update with transitive dependencies
* @return bool
* found that this package actually needs to be updated
*/
*/
private function isUpdateable(PackageInterface $packag e)
private function unfixPackage(Request $request, $name)
{
{
foreach ($this->unfixList as $pattern => $void) {
// remove locked package by this name which was already initialized
$patternRegexp = BasePackage::packageNameToRegexp($pattern);
foreach ($this->packages as $i => $loadedPackage) {
if (preg_match($patternRegexp, $package->getName())) {
if ($loadedPackage->getName() === $name & & $loadedPackage->getRepository() === $request->getLockedRepository()) {
return true;
$request->unfixPackage($loadedPackage);
unset($this->packages[$i]);
}
}
}
}
return false;
unset($this->skippedLoad[$name]);
unset($this->loadedNames[$name]);
}
}
}
}