Skip to content

Commit

Permalink
Allow using temporary update constraints on all packages (incl non-ro…
Browse files Browse the repository at this point in the history
  • Loading branch information
Seldaek authored and emahorvat52 committed Jan 18, 2023
1 parent 3252cea commit f585487
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 50 deletions.
44 changes: 9 additions & 35 deletions src/Composer/Command/UpdateCommand.php
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -144,20 +145,13 @@ protected function execute(InputInterface $input, OutputInterface $output)
}
}

$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()));

Expand All @@ -166,9 +160,9 @@ protected function execute(InputInterface $input, OutputInterface $output)
}

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)) {
Expand Down Expand Up @@ -232,6 +226,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
->setPreferStable($input->getOption('prefer-stable'))
->setPreferLowest($input->getOption('prefer-lowest'))
->setTemporaryConstraints($temporaryConstraints)
;

if ($input->getOption('no-plugins')) {
Expand Down Expand Up @@ -307,25 +302,4 @@ private function getPackagesInteractively(IOInterface $io, InputInterface $input

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
);
}
}
30 changes: 20 additions & 10 deletions src/Composer/DependencyResolver/PoolBuilder.php
Expand Up @@ -58,6 +58,10 @@ class PoolBuilder
* @phpstan-var array<string, string>
*/
private $rootReferences;
/**
* @var array<string, ConstraintInterface>
*/
private $temporaryConstraints;
/**
* @var ?EventDispatcher
*/
Expand Down Expand Up @@ -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;
Expand All @@ -152,6 +157,7 @@ public function __construct(array $acceptableStabilities, array $stabilityFlags,
$this->eventDispatcher = $eventDispatcher;
$this->poolOptimizer = $poolOptimizer;
$this->io = $io;
$this->temporaryConstraints = $temporaryConstraints;
}

/**
Expand Down Expand Up @@ -234,24 +240,28 @@ public function buildPool(array $repositories, Request $request): Pool
$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 (\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
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]);
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/Composer/DependencyResolver/Problem.php
Expand Up @@ -294,6 +294,16 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req
}
}

$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 {
Expand Down
14 changes: 13 additions & 1 deletion src/Composer/Installer.php
Expand Up @@ -60,6 +60,7 @@
use Composer\Repository\RepositoryManager;
use Composer\Repository\LockArrayRepository;
use Composer\Script\ScriptEvents;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Util\Platform;

/**
Expand Down Expand Up @@ -189,6 +190,9 @@ class Installer
*/
protected $additionalFixedRepository;

/** @var array<string, ConstraintInterface> */
protected $temporaryConstraints = [];

/**
* Constructor
*
Expand Down Expand Up @@ -837,7 +841,7 @@ private function createRepositorySet(bool $forUpdate, PlatformRepository $platfo

$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) {
Expand Down Expand Up @@ -1065,6 +1069,14 @@ public function setAdditionalFixedRepository(RepositoryInterface $additionalFixe
return $this;
}

/**
* @param array<string, ConstraintInterface> $constraints
*/
public function setTemporaryConstraints(array $constraints): void
{
$this->temporaryConstraints = $constraints;
}

/**
* Whether to run in drymode or not
*
Expand Down
20 changes: 18 additions & 2 deletions src/Composer/Repository/RepositorySet.php
Expand Up @@ -73,6 +73,11 @@ class RepositorySet
*/
private $rootRequires;

/**
* @var array<string, ConstraintInterface>
*/
private $temporaryConstraints;

/** @var bool */
private $locked = false;
/** @var bool */
Expand All @@ -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;
Expand All @@ -111,6 +117,8 @@ public function __construct(string $minimumStability = 'stable', array $stabilit
unset($this->rootRequires[$name]);
}
}

$this->temporaryConstraints = $temporaryConstraints;
}

/**
Expand All @@ -132,6 +140,14 @@ public function getRootRequires(): array
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
*
Expand Down Expand Up @@ -247,7 +263,7 @@ public function isPackageAcceptable(array $names, string $stability): bool
*/
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) {
Expand Down
95 changes: 95 additions & 0 deletions tests/Composer/Test/Command/UpdateCommandTest.php
@@ -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
];
}
}
11 changes: 9 additions & 2 deletions tests/Composer/Test/TestCase.php
Expand Up @@ -124,9 +124,16 @@ public function initTempComposer(array $composerJson = [], array $authJson = [])
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;
}
Expand Down

0 comments on commit f585487

Please sign in to comment.