Separate locked packages from fixed packages in request

Locked packages are basically like removable fixed packages, so we still
only load one version, but we do not require their installation unless
something the user needs requires their use. So they automatically get
removed if they are no longer needed on any update.
main
Nils Adermann 4 years ago
parent 73e24ea9fb
commit 74fb313c39

@ -31,13 +31,13 @@ class Pool implements \Countable
protected $packageByName = array();
protected $versionParser;
protected $providerCache = array();
protected $unacceptableFixedPackages;
protected $unacceptableFixedOrLockedPackages;
public function __construct(array $packages = array(), array $unacceptableFixedPackages = array())
public function __construct(array $packages = array(), array $unacceptableFixedOrLockedPackages = array())
{
$this->versionParser = new VersionParser;
$this->setPackages($packages);
$this->unacceptableFixedPackages = $unacceptableFixedPackages;
$this->unacceptableFixedOrLockedPackages = $unacceptableFixedOrLockedPackages;
}
private function setPackages(array $packages)
@ -181,9 +181,9 @@ class Pool implements \Countable
return false;
}
public function isUnacceptableFixedPackage(PackageInterface $package)
public function isUnacceptableFixedOrLockedPackage(PackageInterface $package)
{
return \in_array($package, $this->unacceptableFixedPackages, true);
return \in_array($package, $this->unacceptableFixedOrLockedPackages, true);
}
public function __toString()

@ -81,7 +81,7 @@ class PoolBuilder
/**
* @psalm-var list<Package>
*/
private $unacceptableFixedPackages = array();
private $unacceptableFixedOrLockedPackages = array();
private $updateAllowList = array();
private $skippedLoad = array();
@ -90,7 +90,7 @@ class PoolBuilder
* have already their maximum required range loaded and can not be
* extended by markPackageNameForLoading
*
* Packages get cleared from this list if they get unfixed as in that case
* Packages get cleared from this list if they get unlocked as in that case
* we need to actually load them
*/
private $maxExtendedReqs = array();
@ -123,16 +123,14 @@ class PoolBuilder
public function buildPool(array $repositories, Request $request)
{
$singleVersionPackages = $request->getFixedPackages();
if ($request->getUpdateAllowList()) {
$this->updateAllowList = $request->getUpdateAllowList();
$this->warnAboutNonMatchingUpdateAllowList($request);
foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) {
if (!$this->isUpdateAllowed($lockedPackage)) {
//$request->fixPackage($lockedPackage);
$singleVersionPackages[] = $lockedPackage;
$request->lockPackage($lockedPackage);
$fixedOrLockedPackages[] = $lockedPackage;
$lockedName = $lockedPackage->getName();
// remember which packages we skipped loading remote content for in this partial update
$this->skippedLoad[$lockedName] = $lockedName;
@ -143,7 +141,7 @@ class PoolBuilder
}
}
foreach ($singleVersionPackages as $package) {
foreach ($request->getFixedOrLockedPackages() as $package) {
// using MatchAllConstraint here because fixed packages do not need to retrigger
// loading any packages
$this->loadedPackages[$package->getName()] = new MatchAllConstraint();
@ -163,12 +161,12 @@ class PoolBuilder
) {
$this->loadPackage($request, $package, false);
} else {
$this->unacceptableFixedPackages[] = $package;
$this->unacceptableFixedOrLockedPackages[] = $package;
}
}
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 and locked packages have already been added, so if a root require needs one of them, no need to do anything
if (isset($this->loadedPackages[$packageName])) {
continue;
}
@ -222,21 +220,21 @@ class PoolBuilder
$this->rootAliases,
$this->rootReferences,
$this->packages,
$this->unacceptableFixedPackages
$this->unacceptableFixedOrLockedPackages
);
$this->eventDispatcher->dispatch($prePoolCreateEvent->getName(), $prePoolCreateEvent);
$this->packages = $prePoolCreateEvent->getPackages();
$this->unacceptableFixedPackages = $prePoolCreateEvent->getUnacceptableFixedPackages();
$this->unacceptableFixedOrLockedPackages = $prePoolCreateEvent->getUnacceptableFixedPackages();
}
$pool = new Pool($this->packages, $this->unacceptableFixedPackages);
$pool = new Pool($this->packages, $this->unacceptableFixedOrLockedPackages);
$this->aliasMap = array();
$this->packagesToLoad = array();
$this->loadedPackages = array();
$this->loadedPerRepo = array();
$this->packages = array();
$this->unacceptableFixedPackages = array();
$this->unacceptableFixedOrLockedPackages = array();
$this->maxExtendedReqs = array();
$this->skippedLoad = array();
$this->indexCounter = 0;
@ -253,7 +251,7 @@ class PoolBuilder
return;
}
// Root require (which was not unfixed) already loaded the maximum range so no
// Root require (which was not unlocked) already loaded the maximum range so no
// need to check anything here
if (isset($this->maxExtendedReqs[$name])) {
return;
@ -261,7 +259,7 @@ class PoolBuilder
// Root requires can not be overruled by dependencies so there is no point in
// extending the loaded constraint for those.
// This is triggered when loading a root require which was fixed but got unfixed, then
// This is triggered when loading a root require which was locked but got unlocked, then
// we make sure that we load at most the intervals covered by the root constraint.
$rootRequires = $request->getRequires();
if (isset($rootRequires[$name]) && !Intervals::isSubsetOf($constraint, $rootRequires[$name])) {
@ -314,7 +312,7 @@ class PoolBuilder
break;
}
// these repos have their packages fixed if they need to be loaded so we
// these repos have their packages fixed or locked if they need to be loaded so we
// never need to load anything else from them
if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) {
continue;
@ -347,14 +345,14 @@ class PoolBuilder
// 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->getLockedRepository() !== $package->getRepository() && !$request->isFixedPackage($package)) {
// do not modify the references on already locked or fixed packages
if (!$request->isLockedPackage($package) && !$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 propogateUpdate is false we are loading a fixed or locked 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) {
@ -375,11 +373,11 @@ class PoolBuilder
$linkConstraint = $link->getConstraint();
if ($propagateUpdate) {
// if this is a partial update with transitive dependencies we need to unfix the package we now know is a
// if this is a partial update with transitive dependencies we need to unlock 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 ($request->getUpdateAllowTransitiveDependencies() && isset($this->skippedLoad[$require])) {
if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$require])) {
$this->unfixPackage($request, $require);
$this->unlockPackage($request, $require);
$this->markPackageNameForLoading($request, $require, $linkConstraint);
} elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $require) && !isset($this->updateAllowWarned[$require])) {
$this->updateAllowWarned[$require] = true;
@ -389,7 +387,7 @@ class PoolBuilder
$this->markPackageNameForLoading($request, $require, $linkConstraint);
}
} else {
// We also need to load the requirements of a fixed package
// We also need to load the requirements of a locked package
// unless it was skipped
if (!isset($this->skippedLoad[$require])) {
$this->markPackageNameForLoading($request, $require, $linkConstraint);
@ -397,14 +395,14 @@ class PoolBuilder
}
}
// 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 we're doing a partial update with deps we also need to unlock 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->loadedPackages[$replace], $this->skippedLoad[$replace])) {
if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$replace])) {
$this->unfixPackage($request, $replace);
$this->unlockPackage($request, $replace);
$this->markPackageNameForLoading($request, $replace, $link->getConstraint());
} elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$replace])) {
$this->updateAllowWarned[$replace] = true;
@ -467,16 +465,16 @@ class PoolBuilder
}
/**
* Reverts the decision to use a fixed package from lock file if a partial update with transitive dependencies
* Reverts the decision to use a locked package if a partial update with transitive dependencies
* found that this package actually needs to be updated
*/
private function unfixPackage(Request $request, $name)
private function unlockPackage(Request $request, $name)
{
// remove locked package by this name which was already initialized
foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) {
foreach ($request->getLockedPackages() as $lockedPackage) {
if (!($lockedPackage instanceof AliasPackage) && $lockedPackage->getName() === $name) {
if (false !== $index = array_search($lockedPackage, $this->packages, true)) {
//$request->unfixPackage($lockedPackage);
$request->unlockPackage($lockedPackage);
$this->removeLoadedPackage($request, $lockedPackage, $index);
}
}
@ -488,7 +486,7 @@ class PoolBuilder
// as long as it was not unfixed yet
&& isset($this->skippedLoad[$this->skippedLoad[$name]])
) {
$this->unfixPackage($request, $this->skippedLoad[$name]);
$this->unlockPackage($request, $this->skippedLoad[$name]);
}
unset($this->skippedLoad[$name], $this->loadedPackages[$name], $this->maxExtendedReqs[$name]);
@ -499,7 +497,7 @@ class PoolBuilder
unset($this->packages[$index]);
if (isset($this->aliasMap[spl_object_hash($package)])) {
foreach ($this->aliasMap[spl_object_hash($package)] as $aliasIndex => $aliasPackage) {
//$request->unfixPackage($aliasPackage);
$request->unlockPackage($aliasPackage);
unset($this->packages[$aliasIndex]);
}
unset($this->aliasMap[spl_object_hash($package)]);

@ -229,11 +229,11 @@ class Problem
return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.');
}
$fixedPackage = null;
foreach ($request->getFixedPackages() as $package) {
$lockedPackage = null;
foreach ($request->getLockedPackages() as $package) {
if ($package->getName() === $packageName) {
$fixedPackage = $package;
if ($pool->isUnacceptableFixedPackage($package)) {
$lockedPackage = $package;
if ($pool->isUnacceptableFixedOrLockedPackage($package)) {
return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command.');
}
break;
@ -253,13 +253,13 @@ class Problem
}
}
if ($fixedPackage) {
$fixedConstraint = new Constraint('==', $fixedPackage->getVersion());
if ($lockedPackage) {
$fixedConstraint = new Constraint('==', $lockedPackage->getVersion());
$filtered = array_filter($packages, function ($p) use ($fixedConstraint) {
return $fixedConstraint->matches(new Constraint('==', $p->getVersion()));
});
if (0 === count($filtered)) {
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.');
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but the package is fixed to '.$lockedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.');
}
}

@ -44,7 +44,7 @@ class Request
protected $lockedRepository;
protected $requires = array();
protected $fixedPackages = array();
protected $unlockables = array();
protected $lockedPackages = array();
protected $updateAllowList = array();
protected $updateAllowTransitiveDependencies = false;
@ -71,18 +71,22 @@ class Request
*
* @param bool $lockable if set to false, the package will not be written to the lock file
*/
public function fixPackage(PackageInterface $package, $lockable = true)
public function fixPackage(PackageInterface $package)
{
$this->fixedPackages[spl_object_hash($package)] = $package;
}
if (!$lockable) {
$this->unlockables[spl_object_hash($package)] = $package;
}
/**
* Mark an existing package as installed but removable
*/
public function lockPackage(PackageInterface $package)
{
$this->lockedPackages[spl_object_hash($package)] = $package;
}
public function unfixPackage(PackageInterface $package)
public function unlockPackage(PackageInterface $package)
{
unset($this->fixedPackages[spl_object_hash($package)], $this->unlockables[spl_object_hash($package)]);
unset($this->lockedPackages[spl_object_hash($package)]);
}
public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies)
@ -121,6 +125,21 @@ class Request
return isset($this->fixedPackages[spl_object_hash($package)]);
}
public function getLockedPackages()
{
return $this->lockedPackages;
}
public function isLockedPackage(PackageInterface $package)
{
return isset($this->lockedPackages[spl_object_hash($package)]);
}
public function getFixedOrLockedPackages()
{
return array_merge($this->fixedPackages, $this->lockedPackages);
}
// TODO look into removing the packageIds option, the only place true is used is for the installed map in the solver problems
// some locked packages may not be in the pool so they have a package->id of -1
public function getPresentMap($packageIds = false)
@ -140,15 +159,15 @@ class Request
return $presentMap;
}
public function getUnlockableMap()
public function getFixedPackagesMap()
{
$unlockableMap = array();
$fixedPackagesMap = array();
foreach ($this->unlockables as $package) {
$unlockableMap[$package->id] = $package;
foreach ($this->fixedPackages as $package) {
$fixedPackagesMap[$package->id] = $package;
}
return $unlockableMap;
return $fixedPackagesMap;
}
public function getLockedRepository()

@ -125,23 +125,25 @@ abstract class Rule
public function isCausedByLock(RepositorySet $repositorySet, Request $request, Pool $pool)
{
if ($this->getReason() === self::RULE_FIXED && $this->reasonData['lockable']) {
return true;
}
if ($this->getReason() === self::RULE_PACKAGE_REQUIRES) {
if (PlatformRepository::isPlatformPackage($this->reasonData->getTarget())) {
return false;
}
foreach ($request->getFixedPackages() as $package) {
if ($package->getName() === $this->reasonData->getTarget()) {
if ($pool->isUnacceptableFixedPackage($package)) {
return true;
}
if (!$this->reasonData->getConstraint()->matches(new Constraint('=', $package->getVersion()))) {
return true;
if ($request->getLockedRepository()) {
foreach ($request->getLockedRepository()->getPackages() as $package) {
if ($package->getName() === $this->reasonData->getTarget()) {
if ($pool->isUnacceptableFixedOrLockedPackage($package)) {
return true;
}
if (!$this->reasonData->getConstraint()->matches(new Constraint('=', $package->getVersion()))) {
return true;
}
// required package was locked but has been unlocked and still matches
if (!$request->isLockedPackage($package)) {
return true;
}
break;
}
break;
}
}
}
@ -150,15 +152,17 @@ abstract class Rule
if (PlatformRepository::isPlatformPackage($this->reasonData['packageName'])) {
return false;
}
foreach ($request->getFixedPackages() as $package) {
if ($package->getName() === $this->reasonData['packageName']) {
if ($pool->isUnacceptableFixedPackage($package)) {
return true;
}
if (!$this->reasonData['constraint']->matches(new Constraint('=', $package->getVersion()))) {
return true;
if ($request->getLockedRepository()) {
foreach ($request->getLockedRepository()->getPackages() as $package) {
if ($package->getName() === $this->reasonData['packageName']) {
if ($pool->isUnacceptableFixedOrLockedPackage($package)) {
return true;
}
if (!$this->reasonData['constraint']->matches(new Constraint('=', $package->getVersion()))) {
return true;
}
break;
}
break;
}
}
}
@ -180,13 +184,20 @@ abstract class Rule
return 'No package found to satisfy root composer.json require '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '');
}
$packagesNonAlias = array_values(array_filter($packages, function ($p) {
return !($p instanceof AliasPackage);
}));
if (count($packagesNonAlias) === 1) {
$package = $packagesNonAlias[0];
if (!($package instanceof AliasPackage) && $request->isLockedPackage($package)) {
return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion()." and an update of this package was not requested.";
}
}
return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages, $isVerbose).'.';
case self::RULE_FIXED:
$package = $this->deduplicateDefaultBranchAlias($this->reasonData['package']);
if ($this->reasonData['lockable']) {
return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion().' and an update of this package was not requested.';
}
return $package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer';

@ -223,12 +223,10 @@ class RuleSetGenerator
protected function addRulesForRequest(Request $request, $ignorePlatformReqs)
{
$unlockableMap = $request->getUnlockableMap();
foreach ($request->getFixedPackages() as $package) {
if ($package->id == -1) {
// fixed package was not added to the pool as it did not pass the stability requirements, this is fine
if ($this->pool->isUnacceptableFixedPackage($package)) {
if ($this->pool->isUnacceptableFixedOrLockedPackage($package)) {
continue;
}
@ -240,7 +238,6 @@ class RuleSetGenerator
$rule = $this->createInstallOneOfRule(array($package), Rule::RULE_FIXED, array(
'package' => $package,
'lockable' => !isset($unlockableMap[$package->id]),
));
$this->addRule(RuleSet::TYPE_REQUEST, $rule);
}

@ -215,7 +215,7 @@ class Solver
throw new SolverProblemsException($this->problems, $this->learnedPool);
}
return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions);
return new LockTransaction($this->pool, $request->getPresentMap(), $request->getFixedPackagesMap(), $this->decisions);
}
/**

@ -821,9 +821,9 @@ class Installer
{
$request = new Request($lockedRepository);
$request->fixPackage($rootPackage, false);
$request->fixPackage($rootPackage);
if ($rootPackage instanceof RootAliasPackage) {
$request->fixPackage($rootPackage->getAliasOf(), false);
$request->fixPackage($rootPackage->getAliasOf());
}
$fixedPackages = $platformRepo->getPackages();
@ -841,7 +841,7 @@ class Installer
|| !isset($provided[$package->getName()])
|| !$provided[$package->getName()]->getConstraint()->matches(new Constraint('=', $package->getVersion()))
) {
$request->fixPackage($package, false);
$request->fixPackage($package);
}
}

@ -43,9 +43,6 @@ Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- locked/pkg is locked to version dev-master and an update of this package was not requested.
- locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file.
Problem 2
- locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file.
- locked/pkg is locked to version dev-master and an update of this package was not requested.

@ -1,5 +1,5 @@
--TEST--
If a new requirement cannot be installed on a partial update due to replace, there should be a suggestion to use --with-all-dependencies
A partial update for a new package yet to be installed should remove another dependency it replaces if that allows installation.
--COMPOSER--
{
"repositories": [
@ -39,17 +39,6 @@ If a new requirement cannot be installed on a partial update due to replace, the
}
--RUN--
update new/pkg
--EXPECT-EXIT-CODE--
2
--EXPECT-OUTPUT--
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- current/dep is locked to version 1.0.0 and an update of this package was not requested.
- new/pkg[1.0.0] cannot be installed as that would require removing current/dep[1.0.0]. new/pkg replaces current/dep and thus cannot coexist with it.
- Root composer.json requires new/pkg 1.* -> satisfiable by new/pkg[1.0.0].
Use the option --with-all-dependencies to allow upgrades, downgrades and removals for packages currently locked to specific versions.
--EXPECT--
Removing current/dep (1.0.0)
Installing new/pkg (1.0.0)

@ -45,6 +45,7 @@ Require a new package in the composer.json and updating with its name as an argu
--RUN--
update new/pkg --with-dependencies
--EXPECT--
Removing current/dep-provide (1.0.0)
Removing current/dep (1.0.0)
Installing new/pkg (1.0.0)
Installing new/pkg-provide (1.0.0)

Loading…
Cancel
Save