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\PluginEvents;
use Composer\Package\Version\VersionParser;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Util\HttpDownloader;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\Package\Link;
@ -144,20 +145,13 @@ EOT
}
}
$rootPackage = $composer->getPackage();
$rootRequires = $rootPackage->getRequires();
$rootDevRequires = $rootPackage->getDevRequires();
$parser = new VersionParser;
$temporaryConstraints = [];
foreach ($reqs as $package => $constraint) {
if (isset($rootRequires[$package])) {
$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');
$temporaryConstraints[strtolower($package)] = $parser->parseConstraints($constraint);
}
}
$rootPackage->setRequires($rootRequires);
$rootPackage->setDevRequires($rootDevRequires);
$rootPackage = $composer->getPackage();
$rootPackage->setReferences(RootPackageLoader::extractReferences($reqs, $rootPackage->getReferences()));
$rootPackage->setStabilityFlags(RootPackageLoader::extractStabilityFlags($reqs, $rootPackage->getMinimumStability(), $rootPackage->getStabilityFlags()));
@ -166,9 +160,9 @@ EOT
}
if ($input->getOption('root-reqs')) {
$requires = array_keys($rootRequires);
$requires = array_keys($rootPackage->getRequires());
if (!$input->getOption('no-dev')) {
$requires = array_merge($requires, array_keys($rootDevRequires));
$requires = array_merge($requires, array_keys($rootPackage->getDevRequires()));
}
if (!empty($packages)) {
@ -232,6 +226,7 @@ EOT
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
->setPreferStable($input->getOption('prefer-stable'))
->setPreferLowest($input->getOption('prefer-lowest'))
->setTemporaryConstraints($temporaryConstraints)
;
if ($input->getOption('no-plugins')) {
@ -307,25 +302,4 @@ EOT
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>
*/
private $rootReferences;
/**
* @var array<string, ConstraintInterface>
*/
private $temporaryConstraints;
/**
* @var ?EventDispatcher
*/
@ -142,8 +146,9 @@ class PoolBuilder
* @phpstan-param array<string, array<string, array{alias: string, alias_normalized: string}>> $rootAliases
* @param string[] $rootReferences an array of package name => source reference
* @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->stabilityFlags = $stabilityFlags;
@ -152,6 +157,7 @@ class PoolBuilder
$this->eventDispatcher = $eventDispatcher;
$this->poolOptimizer = $poolOptimizer;
$this->io = $io;
$this->temporaryConstraints = $temporaryConstraints;
}
/**
@ -234,24 +240,28 @@ class PoolBuilder
$this->loadPackagesMarkedForLoading($request, $repositories);
}
if (\count($this->temporaryConstraints) > 0) {
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->temporaryConstraints[$package->getName()]) || $package instanceof AliasPackage) {
continue;
}
$constraint = $this->temporaryConstraints[$package->getName()];
$packageAndAliases = array($i => $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;
foreach ($aliasedPackages as $packageOrAlias) {
foreach ($packageAndAliases as $packageOrAlias) {
if (CompilingMatcher::match($constraint, Constraint::OP_EQ, $packageOrAlias->getVersion())) {
$found = true;
}
}
if (!$found) {
foreach ($aliasedPackages as $index => $packageOrAlias) {
foreach ($packageAndAliases as $index => $packageOrAlias) {
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) {
$fixedConstraint = new Constraint('==', $lockedPackage->getVersion());
$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\LockArrayRepository;
use Composer\Script\ScriptEvents;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Util\Platform;
/**
@ -189,6 +190,9 @@ class Installer
*/
protected $additionalFixedRepository;
/** @var array<string, ConstraintInterface> */
protected $temporaryConstraints = [];
/**
* Constructor
*
@ -837,7 +841,7 @@ class Installer
$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($platformRepo);
if ($this->additionalFixedRepository) {
@ -1065,6 +1069,14 @@ class Installer
return $this;
}
/**
* @param array<string, ConstraintInterface> $constraints
*/
public function setTemporaryConstraints(array $constraints): void
{
$this->temporaryConstraints = $constraints;
}
/**
* Whether to run in drymode or not
*

View File

@ -73,6 +73,11 @@ class RepositorySet
*/
private $rootRequires;
/**
* @var array<string, ConstraintInterface>
*/
private $temporaryConstraints;
/** @var bool */
private $locked = false;
/** @var bool */
@ -92,8 +97,9 @@ class RepositorySet
* @phpstan-param array<string, string> $rootReferences
* @param ConstraintInterface[] $rootRequires an array of package name => constraint from the root package
* @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->rootReferences = $rootReferences;
@ -111,6 +117,8 @@ class RepositorySet
unset($this->rootRequires[$name]);
}
}
$this->temporaryConstraints = $temporaryConstraints;
}
/**
@ -132,6 +140,14 @@ class RepositorySet
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
*
@ -247,7 +263,7 @@ class RepositorySet
*/
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) {
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_DISABLE_XDEBUG_WARN', '1');
if ($composerJson === []) {
$composerJson = new \stdClass;
}
if ($authJson === []) {
$authJson = new \stdClass;
}
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.'/auth.json', JsonFile::encode($authJson, 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));
return $dir;
}