1
0
Fork 0

Allow using temporary update constraints on all packages (incl non-root), fixes #10436 (#10773)

pull/10809/head
Jordi Boggiano 2022-05-27 14:51:46 +02:00 committed by GitHub
parent d971f2e37e
commit 556450b15b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 174 additions and 50 deletions

View File

@ -21,6 +21,7 @@ use Composer\Pcre\Preg;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Util\HttpDownloader; use Composer\Util\HttpDownloader;
use Composer\Semver\Constraint\MultiConstraint; use Composer\Semver\Constraint\MultiConstraint;
use Composer\Package\Link; use Composer\Package\Link;
@ -144,20 +145,13 @@ EOT
} }
} }
$rootPackage = $composer->getPackage(); $parser = new VersionParser;
$rootRequires = $rootPackage->getRequires(); $temporaryConstraints = [];
$rootDevRequires = $rootPackage->getDevRequires();
foreach ($reqs as $package => $constraint) { foreach ($reqs as $package => $constraint) {
if (isset($rootRequires[$package])) { $temporaryConstraints[strtolower($package)] = $parser->parseConstraints($constraint);
$rootRequires[$package] = $this->appendConstraintToLink($rootRequires[$package], $constraint);
} elseif (isset($rootDevRequires[$package])) {
$rootDevRequires[$package] = $this->appendConstraintToLink($rootDevRequires[$package], $constraint);
} else {
throw new \UnexpectedValueException('Only root package requirements can receive temporary constraints and '.$package.' is not one');
}
} }
$rootPackage->setRequires($rootRequires);
$rootPackage->setDevRequires($rootDevRequires); $rootPackage = $composer->getPackage();
$rootPackage->setReferences(RootPackageLoader::extractReferences($reqs, $rootPackage->getReferences())); $rootPackage->setReferences(RootPackageLoader::extractReferences($reqs, $rootPackage->getReferences()));
$rootPackage->setStabilityFlags(RootPackageLoader::extractStabilityFlags($reqs, $rootPackage->getMinimumStability(), $rootPackage->getStabilityFlags())); $rootPackage->setStabilityFlags(RootPackageLoader::extractStabilityFlags($reqs, $rootPackage->getMinimumStability(), $rootPackage->getStabilityFlags()));
@ -166,9 +160,9 @@ EOT
} }
if ($input->getOption('root-reqs')) { if ($input->getOption('root-reqs')) {
$requires = array_keys($rootRequires); $requires = array_keys($rootPackage->getRequires());
if (!$input->getOption('no-dev')) { if (!$input->getOption('no-dev')) {
$requires = array_merge($requires, array_keys($rootDevRequires)); $requires = array_merge($requires, array_keys($rootPackage->getDevRequires()));
} }
if (!empty($packages)) { if (!empty($packages)) {
@ -232,6 +226,7 @@ EOT
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input)) ->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
->setPreferStable($input->getOption('prefer-stable')) ->setPreferStable($input->getOption('prefer-stable'))
->setPreferLowest($input->getOption('prefer-lowest')) ->setPreferLowest($input->getOption('prefer-lowest'))
->setTemporaryConstraints($temporaryConstraints)
; ;
if ($input->getOption('no-plugins')) { if ($input->getOption('no-plugins')) {
@ -307,25 +302,4 @@ EOT
throw new \RuntimeException('Installation aborted.'); throw new \RuntimeException('Installation aborted.');
} }
/**
* @param string $constraint
* @return Link
*/
private function appendConstraintToLink(Link $link, string $constraint): Link
{
$parser = new VersionParser;
$oldPrettyString = $link->getConstraint()->getPrettyString();
$newConstraint = MultiConstraint::create(array($link->getConstraint(), $parser->parseConstraints($constraint)));
$newConstraint->setPrettyString($oldPrettyString.', '.$constraint);
return new Link(
$link->getSource(),
$link->getTarget(),
$newConstraint,
/** @phpstan-ignore-next-line */
$link->getDescription(),
$link->getPrettyConstraint() . ', ' . $constraint
);
}
} }

View File

@ -58,6 +58,10 @@ class PoolBuilder
* @phpstan-var array<string, string> * @phpstan-var array<string, string>
*/ */
private $rootReferences; private $rootReferences;
/**
* @var array<string, ConstraintInterface>
*/
private $temporaryConstraints;
/** /**
* @var ?EventDispatcher * @var ?EventDispatcher
*/ */
@ -142,8 +146,9 @@ class PoolBuilder
* @phpstan-param array<string, array<string, array{alias: string, alias_normalized: string}>> $rootAliases * @phpstan-param array<string, array<string, array{alias: string, alias_normalized: string}>> $rootAliases
* @param string[] $rootReferences an array of package name => source reference * @param string[] $rootReferences an array of package name => source reference
* @phpstan-param array<string, string> $rootReferences * @phpstan-param array<string, string> $rootReferences
* @param array<string, ConstraintInterface> $temporaryConstraints Runtime temporary constraints that will be used to filter packages
*/ */
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null) public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null, array $temporaryConstraints = [])
{ {
$this->acceptableStabilities = $acceptableStabilities; $this->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags; $this->stabilityFlags = $stabilityFlags;
@ -152,6 +157,7 @@ class PoolBuilder
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->poolOptimizer = $poolOptimizer; $this->poolOptimizer = $poolOptimizer;
$this->io = $io; $this->io = $io;
$this->temporaryConstraints = $temporaryConstraints;
} }
/** /**
@ -234,24 +240,28 @@ class PoolBuilder
$this->loadPackagesMarkedForLoading($request, $repositories); $this->loadPackagesMarkedForLoading($request, $repositories);
} }
foreach ($this->packages as $i => $package) { if (\count($this->temporaryConstraints) > 0) {
// we check all alias related packages at once, so no need to check individual aliases foreach ($this->packages as $i => $package) {
// isset also checks non-null value // we check all alias related packages at once, so no need to check individual aliases
if (!$package instanceof AliasPackage) { if (!isset($this->temporaryConstraints[$package->getName()]) || $package instanceof AliasPackage) {
$constraint = new Constraint('==', $package->getVersion()); continue;
$aliasedPackages = array($i => $package); }
$constraint = $this->temporaryConstraints[$package->getName()];
$packageAndAliases = array($i => $package);
if (isset($this->aliasMap[spl_object_hash($package)])) { if (isset($this->aliasMap[spl_object_hash($package)])) {
$aliasedPackages += $this->aliasMap[spl_object_hash($package)]; $packageAndAliases += $this->aliasMap[spl_object_hash($package)];
} }
$found = false; $found = false;
foreach ($aliasedPackages as $packageOrAlias) { foreach ($packageAndAliases as $packageOrAlias) {
if (CompilingMatcher::match($constraint, Constraint::OP_EQ, $packageOrAlias->getVersion())) { if (CompilingMatcher::match($constraint, Constraint::OP_EQ, $packageOrAlias->getVersion())) {
$found = true; $found = true;
} }
} }
if (!$found) { if (!$found) {
foreach ($aliasedPackages as $index => $packageOrAlias) { foreach ($packageAndAliases as $index => $packageOrAlias) {
unset($this->packages[$index]); unset($this->packages[$index]);
} }
} }

View File

@ -294,6 +294,16 @@ class Problem
} }
} }
$tempReqs = $repositorySet->getTemporaryConstraints();
if (isset($tempReqs[$packageName])) {
$filtered = array_filter($packages, function ($p) use ($tempReqs, $packageName): bool {
return $tempReqs[$packageName]->matches(new Constraint('==', $p->getVersion()));
});
if (0 === count($filtered)) {
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your temporary update constraint ('.$packageName.':'.$tempReqs[$packageName]->getPrettyString().').');
}
}
if ($lockedPackage) { if ($lockedPackage) {
$fixedConstraint = new Constraint('==', $lockedPackage->getVersion()); $fixedConstraint = new Constraint('==', $lockedPackage->getVersion());
$filtered = array_filter($packages, function ($p) use ($fixedConstraint): bool { $filtered = array_filter($packages, function ($p) use ($fixedConstraint): bool {

View File

@ -60,6 +60,7 @@ use Composer\Repository\RepositoryInterface;
use Composer\Repository\RepositoryManager; use Composer\Repository\RepositoryManager;
use Composer\Repository\LockArrayRepository; use Composer\Repository\LockArrayRepository;
use Composer\Script\ScriptEvents; use Composer\Script\ScriptEvents;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Util\Platform; use Composer\Util\Platform;
/** /**
@ -189,6 +190,9 @@ class Installer
*/ */
protected $additionalFixedRepository; protected $additionalFixedRepository;
/** @var array<string, ConstraintInterface> */
protected $temporaryConstraints = [];
/** /**
* Constructor * Constructor
* *
@ -837,7 +841,7 @@ class Installer
$stabilityFlags[$this->package->getName()] = BasePackage::$stabilities[VersionParser::parseStability($this->package->getVersion())]; $stabilityFlags[$this->package->getName()] = BasePackage::$stabilities[VersionParser::parseStability($this->package->getVersion())];
$repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $rootAliases, $this->package->getReferences(), $rootRequires); $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $rootAliases, $this->package->getReferences(), $rootRequires, $this->temporaryConstraints);
$repositorySet->addRepository(new RootPackageRepository($this->fixedRootPackage)); $repositorySet->addRepository(new RootPackageRepository($this->fixedRootPackage));
$repositorySet->addRepository($platformRepo); $repositorySet->addRepository($platformRepo);
if ($this->additionalFixedRepository) { if ($this->additionalFixedRepository) {
@ -1065,6 +1069,14 @@ class Installer
return $this; return $this;
} }
/**
* @param array<string, ConstraintInterface> $constraints
*/
public function setTemporaryConstraints(array $constraints): void
{
$this->temporaryConstraints = $constraints;
}
/** /**
* Whether to run in drymode or not * Whether to run in drymode or not
* *

View File

@ -73,6 +73,11 @@ class RepositorySet
*/ */
private $rootRequires; private $rootRequires;
/**
* @var array<string, ConstraintInterface>
*/
private $temporaryConstraints;
/** @var bool */ /** @var bool */
private $locked = false; private $locked = false;
/** @var bool */ /** @var bool */
@ -92,8 +97,9 @@ class RepositorySet
* @phpstan-param array<string, string> $rootReferences * @phpstan-param array<string, string> $rootReferences
* @param ConstraintInterface[] $rootRequires an array of package name => constraint from the root package * @param ConstraintInterface[] $rootRequires an array of package name => constraint from the root package
* @phpstan-param array<string, ConstraintInterface> $rootRequires * @phpstan-param array<string, ConstraintInterface> $rootRequires
* @param array<string, ConstraintInterface> $temporaryConstraints Runtime temporary constraints that will be used to filter packages
*/ */
public function __construct(string $minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array()) public function __construct(string $minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array(), array $temporaryConstraints = [])
{ {
$this->rootAliases = self::getRootAliasesPerPackage($rootAliases); $this->rootAliases = self::getRootAliasesPerPackage($rootAliases);
$this->rootReferences = $rootReferences; $this->rootReferences = $rootReferences;
@ -111,6 +117,8 @@ class RepositorySet
unset($this->rootRequires[$name]); unset($this->rootRequires[$name]);
} }
} }
$this->temporaryConstraints = $temporaryConstraints;
} }
/** /**
@ -132,6 +140,14 @@ class RepositorySet
return $this->rootRequires; return $this->rootRequires;
} }
/**
* @return array<string, ConstraintInterface> Runtime temporary constraints that will be used to filter packages
*/
public function getTemporaryConstraints(): array
{
return $this->temporaryConstraints;
}
/** /**
* Adds a repository to this repository set * Adds a repository to this repository set
* *
@ -247,7 +263,7 @@ class RepositorySet
*/ */
public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null): Pool public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null): Pool
{ {
$poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher, $poolOptimizer); $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher, $poolOptimizer, $this->temporaryConstraints);
foreach ($this->repositories as $repo) { foreach ($this->repositories as $repo) {
if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) {

View File

@ -0,0 +1,95 @@
<?php declare(strict_types=1);
/*
* 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\Test\Command;
use Composer\Test\TestCase;
class UpdateCommandTest extends TestCase
{
/**
* @dataProvider provideUpdates
* @param array<mixed> $composerJson
* @param array<mixed> $command
*/
public function testUpdate(array $composerJson, array $command, string $expected): void
{
$this->initTempComposer($composerJson);
$appTester = $this->getApplicationTester();
$appTester->run(array_merge(['command' => 'update', '--dry-run' => true], $command));
$this->assertSame(trim($expected), trim($appTester->getDisplay()));
}
public function provideUpdates(): \Generator
{
$rootDepAndTransitiveDep = [
'repositories' => [
'packages' => [
'type' => 'package',
'package' => [
['name' => 'root/req', 'version' => '1.0.0', 'require' => ['dep/pkg' => '^1']],
['name' => 'dep/pkg', 'version' => '1.0.0'],
['name' => 'dep/pkg', 'version' => '1.0.1'],
['name' => 'dep/pkg', 'version' => '1.0.2'],
],
],
],
'require' => [
'root/req' => '1.*',
],
];
yield 'simple update' => [
$rootDepAndTransitiveDep,
[],
<<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
- Locking dep/pkg (1.0.2)
- Locking root/req (1.0.0)
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing dep/pkg (1.0.2)
- Installing root/req (1.0.0)
OUTPUT
];
yield 'update with temporary constraint + --no-install' => [
$rootDepAndTransitiveDep,
['--with' => ['dep/pkg:1.0.0'], '--no-install' => true],
<<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
- Locking dep/pkg (1.0.0)
- Locking root/req (1.0.0)
OUTPUT
];
yield 'update with temporary constraint failing resolution' => [
$rootDepAndTransitiveDep,
['--with' => ['dep/pkg:^2']],
<<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires root/req 1.* -> satisfiable by root/req[1.0.0].
- root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2).
OUTPUT
];
}
}

View File

@ -124,9 +124,16 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
Platform::putEnv('COMPOSER_HOME', $dir.'/composer-home'); Platform::putEnv('COMPOSER_HOME', $dir.'/composer-home');
Platform::putEnv('COMPOSER_DISABLE_XDEBUG_WARN', '1'); Platform::putEnv('COMPOSER_DISABLE_XDEBUG_WARN', '1');
if ($composerJson === []) {
$composerJson = new \stdClass;
}
if ($authJson === []) {
$authJson = new \stdClass;
}
chdir($dir); chdir($dir);
file_put_contents($dir.'/composer.json', JsonFile::encode($composerJson, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT)); file_put_contents($dir.'/composer.json', JsonFile::encode($composerJson, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
file_put_contents($dir.'/auth.json', JsonFile::encode($authJson, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT)); file_put_contents($dir.'/auth.json', JsonFile::encode($authJson, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $dir; return $dir;
} }