From 6059acf0a39fa97147fa8c26de95b4aa8ea1bcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=B6ller?= Date: Wed, 27 Dec 2017 13:20:12 +0100 Subject: [PATCH 001/321] Fix: Remove unused parameter and field --- src/Composer/Factory.php | 2 +- src/Composer/Package/Locker.php | 5 +-- tests/Composer/Test/InstallerTest.php | 2 +- tests/Composer/Test/Package/LockerTest.php | 44 ++++++---------------- 4 files changed, 14 insertions(+), 39 deletions(-) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index a68586fbb..e1c4cf95c 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -390,7 +390,7 @@ class Factory ? substr($composerFile, 0, -4).'lock' : $composerFile . '.lock'; - $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $rm, $im, file_get_contents($composerFile)); + $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile)); $composer->setLocker($locker); } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 93e5ca655..e86115bfb 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -32,7 +32,6 @@ use Seld\JsonLint\ParsingException; class Locker { private $lockFile; - private $repositoryManager; private $installationManager; private $hash; private $contentHash; @@ -46,14 +45,12 @@ class Locker * * @param IOInterface $io * @param JsonFile $lockFile lockfile loader - * @param RepositoryManager $repositoryManager repository manager instance * @param InstallationManager $installationManager installation manager instance * @param string $composerFileContents The contents of the composer file */ - public function __construct(IOInterface $io, JsonFile $lockFile, RepositoryManager $repositoryManager, InstallationManager $installationManager, $composerFileContents) + public function __construct(IOInterface $io, JsonFile $lockFile, InstallationManager $installationManager, $composerFileContents) { $this->lockFile = $lockFile; - $this->repositoryManager = $repositoryManager; $this->installationManager = $installationManager; $this->hash = md5($composerFileContents); $this->contentHash = self::getContentHash($composerFileContents); diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 468d8fbbd..fe17f2959 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -198,7 +198,7 @@ class InstallerTest extends TestCase } $contents = json_encode($composerConfig); - $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), $contents); + $locker = new Locker($io, $lockJsonMock, $composer->getInstallationManager(), $contents); $composer->setLocker($locker); $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index a37e09907..63fbf4a1b 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -21,7 +21,7 @@ class LockerTest extends TestCase public function testIsLocked() { $json = $this->createJsonFileMock(); - $locker = new Locker(new NullIO, $json, $this->createRepositoryManagerMock(), $this->createInstallationManagerMock(), + $locker = new Locker(new NullIO, $json, $this->createInstallationManagerMock(), $this->getJsonContent()); $json @@ -39,10 +39,9 @@ class LockerTest extends TestCase public function testGetNotLockedPackages() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $json ->expects($this->once()) @@ -57,10 +56,9 @@ class LockerTest extends TestCase public function testGetLockedPackages() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $json ->expects($this->once()) @@ -84,11 +82,10 @@ class LockerTest extends TestCase public function testSetLockData() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); $jsonContent = $this->getJsonContent() . ' '; - $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent); + $locker = new Locker(new NullIO, $json, $inst, $jsonContent); $package1 = $this->createPackageMock(); $package2 = $this->createPackageMock(); @@ -157,10 +154,9 @@ class LockerTest extends TestCase public function testLockBadPackages() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $package1 = $this->createPackageMock(); $package1 @@ -176,11 +172,10 @@ class LockerTest extends TestCase public function testIsFresh() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); $jsonContent = $this->getJsonContent(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent); + $locker = new Locker(new NullIO, $json, $inst, $jsonContent); $json ->expects($this->once()) @@ -193,10 +188,9 @@ class LockerTest extends TestCase public function testIsFreshFalse() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $json ->expects($this->once()) @@ -209,11 +203,10 @@ class LockerTest extends TestCase public function testIsFreshWithContentHash() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); $jsonContent = $this->getJsonContent(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent); + $locker = new Locker(new NullIO, $json, $inst, $jsonContent); $json ->expects($this->once()) @@ -226,11 +219,10 @@ class LockerTest extends TestCase public function testIsFreshWithContentHashAndNoHash() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); $jsonContent = $this->getJsonContent(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $jsonContent); + $locker = new Locker(new NullIO, $json, $inst, $jsonContent); $json ->expects($this->once()) @@ -243,10 +235,9 @@ class LockerTest extends TestCase public function testIsFreshFalseWithContentHash() { $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker(new NullIO, $json, $repo, $inst, $this->getJsonContent()); + $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); $differentHash = md5($this->getJsonContent(array('name' => 'test2'))); @@ -264,20 +255,7 @@ class LockerTest extends TestCase ->disableOriginalConstructor() ->getMock(); } - - private function createRepositoryManagerMock() - { - $mock = $this->getMockBuilder('Composer\Repository\RepositoryManager') - ->disableOriginalConstructor() - ->getMock(); - - $mock->expects($this->any()) - ->method('getLocalRepository') - ->will($this->returnValue($this->getMockBuilder('Composer\Repository\ArrayRepository')->getMock())); - - return $mock; - } - + private function createInstallationManagerMock() { $mock = $this->getMockBuilder('Composer\Installer\InstallationManager') From 6ef65e5319a31c6ad1ad4913af50dae7f2832356 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Mon, 10 Sep 2018 15:23:40 +0200 Subject: [PATCH 002/321] Add a new RepositorySet class and restrict pool usage to the solver Breaking change for the plugin interface so bumping the version of composer-plugin-api to 2.0.0 First step for a refactoring of the package metadata loading mechanism --- .../Command/BaseDependencyCommand.php | 9 +- src/Composer/Command/CreateProjectCommand.php | 7 +- src/Composer/Command/InitCommand.php | 19 ++-- src/Composer/Command/ShowCommand.php | 35 +++---- src/Composer/DependencyResolver/Pool.php | 1 + src/Composer/DependencyResolver/Solver.php | 23 +++-- .../EventDispatcher/EventDispatcher.php | 16 ++-- src/Composer/Installer.php | 90 ++++++++--------- src/Composer/Installer/InstallerEvent.php | 18 ++-- src/Composer/Installer/PackageEvent.php | 7 +- .../Package/Version/VersionSelector.php | 9 +- src/Composer/Plugin/PluginInterface.php | 2 +- src/Composer/Plugin/PluginManager.php | 29 +++--- src/Composer/Repository/RepositorySet.php | 59 ++++++++++++ .../Test/DependencyResolver/SolverTest.php | 19 ++-- .../EventDispatcher/EventDispatcherTest.php | 6 +- .../Test/Installer/InstallerEventTest.php | 6 +- .../Package/Version/VersionSelectorTest.php | 96 ++++++++++--------- .../Plugin/Fixtures/plugin-v1/composer.json | 2 +- .../Plugin/Fixtures/plugin-v2/composer.json | 2 +- .../Plugin/Fixtures/plugin-v3/composer.json | 2 +- .../Plugin/Fixtures/plugin-v4/composer.json | 2 +- .../Plugin/Fixtures/plugin-v8/composer.json | 2 +- .../Plugin/Fixtures/plugin-v9/composer.json | 2 +- 24 files changed, 265 insertions(+), 198 deletions(-) create mode 100644 src/Composer/Repository/RepositorySet.php diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 4c8766ba3..ff7bae2fe 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -21,6 +21,7 @@ use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Composer\Repository\RepositorySet; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Helper\Table; @@ -71,15 +72,15 @@ class BaseDependencyCommand extends BaseCommand $commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); - // Prepare repositories and set up a pool + // Prepare repositories and set up a repo set $platformOverrides = $composer->getConfig()->get('platform') ?: array(); $repository = new CompositeRepository(array( new ArrayRepository(array($composer->getPackage())), $composer->getRepositoryManager()->getLocalRepository(), new PlatformRepository(array(), $platformOverrides), )); - $pool = new Pool(); - $pool->addRepository($repository); + $repositorySet = new RepositorySet(new Pool()); + $repositorySet->addRepository($repository); // Parse package name and constraint list($needle, $textConstraint) = array_pad( @@ -89,7 +90,7 @@ class BaseDependencyCommand extends BaseCommand ); // Find packages that are or provide the requested package first - $packages = $pool->whatProvides(strtolower($needle)); + $packages = $repositorySet->findPackages(strtolower($needle)); // TODO this does not search providers if (empty($packages)) { throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index cca5f1871..4c5a21794 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -28,6 +28,7 @@ use Composer\Repository\RepositoryFactory; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\InstalledFilesystemRepository; +use Composer\Repository\RepositorySet; use Composer\Script\ScriptEvents; use Composer\Util\Silencer; use Symfony\Component\Console\Input\InputArgument; @@ -290,8 +291,8 @@ EOT throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } - $pool = new Pool($stability); - $pool->addRepository($sourceRepo); + $repositorySet = new RepositorySet(new Pool($stability)); + $repositorySet->addRepository($sourceRepo); $phpVersion = null; $prettyPhpVersion = null; @@ -305,7 +306,7 @@ EOT } // find the latest version if there are multiple - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability); if (!$package) { diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index be56b23fb..86c55b152 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -21,6 +21,7 @@ use Composer\Package\Version\VersionSelector; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; +use Composer\Repository\RepositorySet; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -40,8 +41,8 @@ class InitCommand extends BaseCommand /** @var array */ private $gitConfig; - /** @var Pool[] */ - private $pools; + /** @var RepositorySet[] */ + private $repositorySets; /** * {@inheritdoc} @@ -637,16 +638,16 @@ EOT return false !== filter_var($email, FILTER_VALIDATE_EMAIL); } - private function getPool(InputInterface $input, $minimumStability = null) + private function getRepositorySet(InputInterface $input, $minimumStability = null) { $key = $minimumStability ?: 'default'; - if (!isset($this->pools[$key])) { - $this->pools[$key] = $pool = new Pool($minimumStability ?: $this->getMinimumStability($input)); - $pool->addRepository($this->getRepos()); + if (!isset($this->repositorySets[$key])) { + $this->repositorySets[$key] = $repositorySet = new RepositorySet(new Pool($minimumStability ?: $this->getMinimumStability($input))); + $repositorySet->addRepository($this->getRepos()); } - return $this->pools[$key]; + return $this->repositorySets[$key]; } private function getMinimumStability(InputInterface $input) @@ -681,8 +682,8 @@ EOT */ private function findBestVersionAndNameForPackage(InputInterface $input, $name, $phpVersion, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null) { - // find the latest version allowed in this pool - $versionSelector = new VersionSelector($this->getPool($input, $minimumStability)); + // find the latest version allowed in this repo set + $versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability)); $package = $versionSelector->findBestCandidate($name, $requiredVersion, $phpVersion, $preferredStability); // retry without phpVersion if platform requirements are ignored in case nothing was found diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index ccea6a960..edb7198c9 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -29,6 +29,7 @@ use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryInterface; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; @@ -52,8 +53,8 @@ class ShowCommand extends BaseCommand protected $versionParser; protected $colors; - /** @var Pool */ - private $pool; + /** @var RepositorySet */ + private $repositorySet; protected function configure() { @@ -523,19 +524,13 @@ EOT $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version; $policy = new DefaultPolicy(); - $pool = new Pool('dev'); - $pool->addRepository($repos); + $repositorySet = new RepositorySet(new Pool('dev')); + $repositorySet->addRepository($repos); $matchedPackage = null; $versions = array(); - $matches = $pool->whatProvides($name, $constraint); + $matches = $repositorySet->findPackages($name, $constraint); foreach ($matches as $index => $package) { - // skip providers/replacers - if ($package->getName() !== $name) { - unset($matches[$index]); - continue; - } - // select an exact match if it is in the installed repo and no specific version was required if (null === $version && $installedRepo->hasPackage($package)) { $matchedPackage = $package; @@ -546,8 +541,8 @@ EOT } // select preferred package according to policy rules - if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) { - $matchedPackage = $pool->literalToPackage($preferred[0]); + if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($repositorySet->getPoolTemp(), array(), $matches)) { // TODO get rid of the pool call + $matchedPackage = $repositorySet->getPoolTemp()->literalToPackage($preferred[0]); } return array($matchedPackage, $versions); @@ -961,9 +956,9 @@ EOT */ private function findLatestPackage(PackageInterface $package, Composer $composer, $phpVersion, $minorOnly = false) { - // find the latest version allowed in this pool + // find the latest version allowed in this repo set $name = $package->getName(); - $versionSelector = new VersionSelector($this->getPool($composer)); + $versionSelector = new VersionSelector($this->getRepositorySet($composer)); $stability = $composer->getPackage()->getMinimumStability(); $flags = $composer->getPackage()->getStabilityFlags(); if (isset($flags[$name])) { @@ -987,13 +982,13 @@ EOT return $versionSelector->findBestCandidate($name, $targetVersion, $phpVersion, $bestStability); } - private function getPool(Composer $composer) + private function getRepositorySet(Composer $composer) { - if (!$this->pool) { - $this->pool = new Pool($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); - $this->pool->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); + if (!$this->repositorySet) { + $this->repositorySet = new RepositorySet(new Pool($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags())); + $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); } - return $this->pool; + return $this->repositorySet; } } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 085aaa7bf..c63556974 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -15,6 +15,7 @@ namespace Composer\DependencyResolver; use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 1ed35ad9c..f5226fca5 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -15,6 +15,7 @@ namespace Composer\DependencyResolver; use Composer\IO\IOInterface; use Composer\Repository\RepositoryInterface; use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -26,8 +27,8 @@ class Solver /** @var PolicyInterface */ protected $policy; - /** @var Pool */ - protected $pool; + /** @var RepositorySet */ + protected $repositorySet = null; /** @var RepositoryInterface */ protected $installed; /** @var RuleSet */ @@ -36,6 +37,8 @@ class Solver protected $ruleSetGenerator; /** @var array */ protected $jobs; + /** @var Pool */ + protected $pool = null; /** @var int[] */ protected $updateMap = array(); @@ -62,17 +65,16 @@ class Solver /** * @param PolicyInterface $policy - * @param Pool $pool + * @param RepositorySet $repositorySet * @param RepositoryInterface $installed * @param IOInterface $io */ - public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed, IOInterface $io) + public function __construct(PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $installed, IOInterface $io) { $this->io = $io; $this->policy = $policy; - $this->pool = $pool; + $this->repositorySet = $repositorySet; $this->installed = $installed; - $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool); } /** @@ -83,6 +85,11 @@ class Solver return count($this->rules); } + public function getPool() + { + return $this->pool; + } + // aka solver_makeruledecisions private function makeAssertionRuleDecisions() @@ -210,7 +217,11 @@ class Solver { $this->jobs = $request->getJobs(); + $this->pool = $this->repositorySet->createPool(); + $this->setupInstalledMap(); + + $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs); $this->checkForRootRequireProblems($ignorePlatformReqs); $this->decisions = new Decisions($this->pool); diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 145944b07..f0fcdaef6 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -13,13 +13,13 @@ namespace Composer\EventDispatcher; use Composer\DependencyResolver\PolicyInterface; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\Installer\InstallerEvent; use Composer\IO\IOInterface; use Composer\Composer; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositorySet; use Composer\Script; use Composer\Installer\PackageEvent; use Composer\Installer\BinaryInstaller; @@ -102,7 +102,7 @@ class EventDispatcher * @param string $eventName The constant in PackageEvents * @param bool $devMode Whether or not we are in dev mode * @param PolicyInterface $policy The policy - * @param Pool $pool The pool + * @param RepositorySet $repositorySet The repository set * @param CompositeRepository $installedRepo The installed repository * @param Request $request The request * @param array $operations The list of operations @@ -111,9 +111,9 @@ class EventDispatcher * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchPackageEvent($eventName, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation) + public function dispatchPackageEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation) { - return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $policy, $pool, $installedRepo, $request, $operations, $operation)); + return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation)); } /** @@ -122,7 +122,7 @@ class EventDispatcher * @param string $eventName The constant in InstallerEvents * @param bool $devMode Whether or not we are in dev mode * @param PolicyInterface $policy The policy - * @param Pool $pool The pool + * @param RepositorySet $repositorySet The repository set * @param CompositeRepository $installedRepo The installed repository * @param Request $request The request * @param array $operations The list of operations @@ -130,9 +130,9 @@ class EventDispatcher * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array()) + public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations = array()) { - return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $pool, $installedRepo, $request, $operations)); + return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $installedRepo, $request, $operations)); } /** @@ -340,7 +340,7 @@ class EventDispatcher $event->getIO(), $event->isDevMode(), $event->getPolicy(), - $event->getPool(), + $event->getRepositorySet(), $event->getInstalledRepo(), $event->getRequest(), $event->getOperations(), diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index a729710c0..73120ea4b 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -37,6 +37,7 @@ use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\Package\Locker; use Composer\Package\PackageInterface; @@ -368,21 +369,21 @@ class Installer $this->io->writeError('Loading composer repositories with package information'); - // creating repository pool + // creating repository set $policy = $this->createPolicy(); - $pool = $this->createPool($this->update ? null : $lockedRepository); - $pool->addRepository($installedRepo, $aliases); + $repositorySet = $this->createRepositorySet($this->update ? null : $lockedRepository); + $repositorySet->addRepository($installedRepo, $aliases); if ($this->update) { $repositories = $this->repositoryManager->getRepositories(); foreach ($repositories as $repository) { - $pool->addRepository($repository, $aliases); + $repositorySet->addRepository($repository, $aliases); } } // Add the locked repository after the others in case we are doing a // partial update so missing packages can be found there still. // For installs from lock it's the only one added so it is first if ($lockedRepository) { - $pool->addRepository($lockedRepository, $aliases); + $repositorySet->addRepository($lockedRepository, $aliases); } // creating requirements request @@ -393,7 +394,7 @@ class Installer $removedUnstablePackages = array(); foreach ($localRepo->getPackages() as $package) { if ( - !$pool->isPackageAcceptable($package->getNames(), $package->getStability()) + !$repositorySet->isPackageAcceptable($package->getNames(), $package->getStability()) && $this->installationManager->isPackageInstalled($localRepo, $package) ) { $removedUnstablePackages[$package->getName()] = true; @@ -465,11 +466,11 @@ class Installer } // force dev packages to have the latest links if we update or install from a (potentially new) lock - $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); + $this->processDevPackages($localRepo, $repositorySet, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); // solve dependencies - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $pool, $installedRepo, $request); - $solver = new Solver($policy, $pool, $installedRepo, $this->io); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); + $solver = new Solver($policy, $repositorySet, $installedRepo, $this->io); try { $operations = $solver->solve($request, $this->ignorePlatformReqs); } catch (SolverProblemsException $e) { @@ -483,11 +484,10 @@ class Installer } // force dev packages to be updated if we update or install from a (potentially new) lock - $operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-updates', $operations); + $operations = $this->processDevPackages($localRepo, $repositorySet, $policy, $repositories, $installedRepo, $lockedRepository, 'force-updates', $operations); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $pool, $installedRepo, $request, $operations); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations); - $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE); // execute operations @@ -581,7 +581,7 @@ class Installer $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($jobType); if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $pool, $installedRepo, $request, $operations, $operation); + $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); } // output non-alias ops when not executing operations (i.e. dry run), output alias ops in debug verbosity @@ -599,11 +599,11 @@ class Installer if ($reason instanceof Rule) { switch ($reason->getReason()) { case Rule::RULE_JOB_INSTALL: - $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); + $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($solver->getPool())); $this->io->writeError(''); break; case Rule::RULE_PACKAGE_REQUIRES: - $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); + $this->io->writeError(' REASON: '.$reason->getPrettyString($solver->getPool())); $this->io->writeError(''); break; } @@ -612,7 +612,7 @@ class Installer $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $pool, $installedRepo, $request, $operations, $operation); + $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); } if ($this->executeOperations || $this->writeLock) { @@ -622,7 +622,7 @@ class Installer if ($this->executeOperations) { // force source/dist urls to be updated for all packages - $this->processPackageUrls($pool, $policy, $localRepo, $repositories); + $this->processPackageUrls($repositorySet, $policy, $localRepo, $repositories); $localRepo->write(); } @@ -685,9 +685,9 @@ class Installer unset($tempLocalRepo, $loader, $dumper); $policy = $this->createPolicy(); - $pool = $this->createPool(); + $repositorySet = $this->createRepositorySet(); $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); - $pool->addRepository($installedRepo, $aliases); + $repositorySet->addRepository($installedRepo, $aliases); // creating requirements request without dev requirements $request = $this->createRequest($this->package, $platformRepo); @@ -697,10 +697,10 @@ class Installer } // solve deps to see which get removed - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); - $solver = new Solver($policy, $pool, $installedRepo, $this->io); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request); + $solver = new Solver($policy, $repositorySet, $installedRepo, $this->io); $ops = $solver->solve($request, $this->ignorePlatformReqs); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request, $ops); $devPackages = array(); foreach ($ops as $op) { @@ -844,6 +844,12 @@ class Installer return $installedRepo; } + private function createRepositorySet($lockedRepository = null) + { + $pool = $this->createPool($lockedRepository); + return new RepositorySet($pool); + } + /** * @param RepositoryInterface|null $lockedRepository * @return Pool @@ -946,7 +952,7 @@ class Installer /** * @param WritableRepositoryInterface $localRepo - * @param Pool $pool + * @param RepositorySet $repositorySet * @param PolicyInterface $policy * @param array $repositories * @param RepositoryInterface $installedRepo @@ -955,7 +961,7 @@ class Installer * @param array|null $operations * @return array */ - private function processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, $task, array $operations = null) + private function processDevPackages($localRepo, $repositorySet, $policy, $repositories, $installedRepo, $lockedRepository, $task, array $operations = null) { if ($task === 'force-updates' && null === $operations) { throw new \InvalidArgumentException('Missing operations argument'); @@ -1010,7 +1016,7 @@ class Installer } // find similar packages (name/version) in all repositories - $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion())); + $matches = $repositorySet->findPackages($package->getName(), new Constraint('=', $package->getVersion())); foreach ($matches as $index => $match) { // skip local packages if (!in_array($match->getRepository(), $repositories, true)) { @@ -1018,18 +1024,12 @@ class Installer continue; } - // skip providers/replacers - if ($match->getName() !== $package->getName()) { - unset($matches[$index]); - continue; - } - $matches[$index] = $match->getId(); } // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { - $newPackage = $pool->literalToPackage($matches[0]); + if ($matches && $matches = $policy->selectPreferredPackages($repositorySet->getPoolTemp(), array(), $matches)) { // TODO remove temp call + $newPackage = $repositorySet->getPoolTemp()->literalToPackage($matches[0]); if ($task === 'force-links' && $newPackage) { $package->setRequires($newPackage->getRequires()); @@ -1130,12 +1130,12 @@ class Installer } /** - * @param Pool $pool + * @param RepositorySet $repositorySet * @param PolicyInterface $policy * @param WritableRepositoryInterface $localRepo * @param array $repositories */ - private function processPackageUrls($pool, $policy, $localRepo, $repositories) + private function processPackageUrls($repositorySet, $policy, $localRepo, $repositories) { if (!$this->update) { return; @@ -1145,7 +1145,7 @@ class Installer foreach ($localRepo->getCanonicalPackages() as $package) { // find similar packages (name/version) in all repositories - $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion())); + $matches = $repositorySet->findPackages($package->getName(), new Constraint('=', $package->getVersion())); foreach ($matches as $index => $match) { // skip local packages if (!in_array($match->getRepository(), $repositories, true)) { @@ -1153,18 +1153,12 @@ class Installer continue; } - // skip providers/replacers - if ($match->getName() !== $package->getName()) { - unset($matches[$index]); - continue; - } - $matches[$index] = $match->getId(); } // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { - $newPackage = $pool->literalToPackage($matches[0]); + if ($matches && $matches = $policy->selectPreferredPackages($repositorySet->getPoolTemp(), array(), $matches)) { // TODO get rid of pool + $newPackage = $repositorySet->getPoolTemp()->literalToPackage($matches[0]); // update the dist and source URLs $sourceUrl = $package->getSourceUrl(); @@ -1325,8 +1319,8 @@ class Installer } } - $pool = new Pool('dev'); - $pool->addRepository($localOrLockRepo); + $repositorySet = new RepositorySet(new Pool('dev')); + $repositorySet->addRepository($localOrLockRepo); $seen = array(); @@ -1335,7 +1329,7 @@ class Installer foreach ($this->updateWhitelist as $packageName => $void) { $packageQueue = new \SplQueue; - $depPackages = $pool->whatProvides($packageName); + $depPackages = $repositorySet->findPackages($packageName); // TODO does this need replacers/providers? $nameMatchesRequiredPackage = in_array($packageName, $requiredPackageNames, true); @@ -1374,7 +1368,7 @@ class Installer $requires = $package->getRequires(); foreach ($requires as $require) { - $requirePackages = $pool->whatProvides($require->getTarget()); + $requirePackages = $repositorySet->findPackages($require->getTarget()); // TODO does this need replacers/providers? foreach ($requirePackages as $requirePackage) { if (isset($this->updateWhitelist[$requirePackage->getName()])) { diff --git a/src/Composer/Installer/InstallerEvent.php b/src/Composer/Installer/InstallerEvent.php index 87153bd51..2d30940a9 100644 --- a/src/Composer/Installer/InstallerEvent.php +++ b/src/Composer/Installer/InstallerEvent.php @@ -15,11 +15,11 @@ namespace Composer\Installer; use Composer\Composer; use Composer\DependencyResolver\PolicyInterface; use Composer\DependencyResolver\Operation\OperationInterface; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\EventDispatcher\Event; use Composer\IO\IOInterface; use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositorySet; /** * An event for all installer. @@ -49,9 +49,9 @@ class InstallerEvent extends Event private $policy; /** - * @var Pool + * @var RepositorySet */ - private $pool; + private $repositorySet; /** * @var CompositeRepository @@ -76,12 +76,12 @@ class InstallerEvent extends Event * @param IOInterface $io * @param bool $devMode * @param PolicyInterface $policy - * @param Pool $pool + * @param RepositorySet $repositorySet * @param CompositeRepository $installedRepo * @param Request $request * @param OperationInterface[] $operations */ - public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array()) + public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations = array()) { parent::__construct($eventName); @@ -89,7 +89,7 @@ class InstallerEvent extends Event $this->io = $io; $this->devMode = $devMode; $this->policy = $policy; - $this->pool = $pool; + $this->repositorySet = $repositorySet; $this->installedRepo = $installedRepo; $this->request = $request; $this->operations = $operations; @@ -128,11 +128,11 @@ class InstallerEvent extends Event } /** - * @return Pool + * @return RepositorySet */ - public function getPool() + public function getRepositorySet() { - return $this->pool; + return $this->repositorySet; } /** diff --git a/src/Composer/Installer/PackageEvent.php b/src/Composer/Installer/PackageEvent.php index f5cf0ed6e..a563a91ba 100644 --- a/src/Composer/Installer/PackageEvent.php +++ b/src/Composer/Installer/PackageEvent.php @@ -16,7 +16,6 @@ use Composer\Composer; use Composer\IO\IOInterface; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\PolicyInterface; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\Repository\CompositeRepository; @@ -40,15 +39,15 @@ class PackageEvent extends InstallerEvent * @param IOInterface $io * @param bool $devMode * @param PolicyInterface $policy - * @param Pool $pool + * @param RepositorySet $repositorySet * @param CompositeRepository $installedRepo * @param Request $request * @param OperationInterface[] $operations * @param OperationInterface $operation */ - public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation) + public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation) { - parent::__construct($eventName, $composer, $io, $devMode, $policy, $pool, $installedRepo, $request, $operations); + parent::__construct($eventName, $composer, $io, $devMode, $policy, $repositorySet, $installedRepo, $request, $operations); $this->operation = $operation; } diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index 8e225d803..d99780ab1 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -17,6 +17,7 @@ use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; /** @@ -27,13 +28,13 @@ use Composer\Semver\Constraint\Constraint; */ class VersionSelector { - private $pool; + private $repositorySet; private $parser; - public function __construct(Pool $pool) + public function __construct(RepositorySet $repositorySet) { - $this->pool = $pool; + $this->repositorySet = $repositorySet; } /** @@ -49,7 +50,7 @@ class VersionSelector public function findBestCandidate($packageName, $targetPackageVersion = null, $targetPhpVersion = null, $preferredStability = 'stable') { $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; - $candidates = $this->pool->whatProvides(strtolower($packageName), $constraint, true); + $candidates = $this->repositorySet->findPackages(strtolower($packageName), $constraint); if ($targetPhpVersion) { $phpConstraint = new Constraint('==', $this->getParser()->normalize($targetPhpVersion)); diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index 6eaca4e90..5158b66f6 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -27,7 +27,7 @@ interface PluginInterface * * @var string */ - const PLUGIN_API_VERSION = '1.1.0'; + const PLUGIN_API_VERSION = '2.0.0'; /** * Apply plugin modifications to Composer diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index e8f4b58c3..c6bdefc5a 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -21,6 +21,7 @@ use Composer\Repository\RepositoryInterface; use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; use Composer\Package\Link; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\DependencyResolver\Pool; use Composer\Plugin\Capability\Capability; @@ -157,14 +158,14 @@ class PluginManager $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $pool = new Pool('dev'); - $pool->addRepository($localRepo); + $repositorySet = new RepositorySet(new Pool('dev')); + $repositorySet->addRepository($localRepo); if ($globalRepo) { - $pool->addRepository($globalRepo); + $repositorySet->addRepository($globalRepo); } $autoloadPackages = array($package->getName() => $package); - $autoloadPackages = $this->collectDependencies($pool, $autoloadPackages, $package); + $autoloadPackages = $this->collectDependencies($repositorySet, $autoloadPackages, $package); $generator = $this->composer->getAutoloadGenerator(); $autoloads = array(); @@ -269,13 +270,13 @@ class PluginManager /** * Recursively generates a map of package names to packages for all deps * - * @param Pool $pool Package pool of installed packages - * @param array $collected Current state of the map for recursion - * @param PackageInterface $package The package to analyze + * @param RepositorySet $repositorySet Repository set of installed packages + * @param array $collected Current state of the map for recursion + * @param PackageInterface $package The package to analyze * * @return array Map of package names to packages */ - private function collectDependencies(Pool $pool, array $collected, PackageInterface $package) + private function collectDependencies(RepositorySet $repositorySet, array $collected, PackageInterface $package) { $requires = array_merge( $package->getRequires(), @@ -283,10 +284,10 @@ class PluginManager ); foreach ($requires as $requireLink) { - $requiredPackage = $this->lookupInstalledPackage($pool, $requireLink); + $requiredPackage = $this->lookupInstalledPackage($repositorySet, $requireLink); if ($requiredPackage && !isset($collected[$requiredPackage->getName()])) { $collected[$requiredPackage->getName()] = $requiredPackage; - $collected = $this->collectDependencies($pool, $collected, $requiredPackage); + $collected = $this->collectDependencies($repositorySet, $collected, $requiredPackage); } } @@ -294,18 +295,18 @@ class PluginManager } /** - * Resolves a package link to a package in the installed pool + * Resolves a package link to a package in the installed repo set * * Since dependencies are already installed this should always find one. * - * @param Pool $pool Pool of installed packages only + * @param RepositorySet $repositorySet Repository set of installed packages only * @param Link $link Package link to look up * * @return PackageInterface|null The found package */ - private function lookupInstalledPackage(Pool $pool, Link $link) + private function lookupInstalledPackage(RepositorySet $repositorySet, Link $link) { - $packages = $pool->whatProvides($link->getTarget(), $link->getConstraint()); + $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint()); // TODO this no longer returns providers return !empty($packages) ? $packages[0] : null; } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php new file mode 100644 index 000000000..e6b6db26b --- /dev/null +++ b/src/Composer/Repository/RepositorySet.php @@ -0,0 +1,59 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\DependencyResolver\Pool; +use Composer\Package\BasePackage; +use Composer\Package\Version\VersionParser; +use Composer\Repository\CompositeRepository; +use Composer\Repository\PlatformRepository; +use Composer\Semver\Constraint\ConstraintInterface; + +/** + * @author Nils Adermann + */ +class RepositorySet +{ + private $pool; + + public function __construct(Pool $pool) + { + $this->pool = $pool; + } + + public function addRepository(RepositoryInterface $repo, $rootAliases = array()) + { + return $this->pool->addRepository($repo, $rootAliases); + } + + public function isPackageAcceptable($name, $stability) + { + return $this->pool->isPackageAcceptable($name, $stability); + } + + public function findPackages($name, ConstraintInterface $constraint = null) + { + return $this->pool->whatProvides($name, $constraint, true); + } + + public function createPool() + { + return $this->pool; + } + + // TODO get rid of this function + public function getPoolTemp() + { + return $this->pool; + } +} diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 28c439b9e..0142818fc 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -20,12 +20,13 @@ use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Solver; use Composer\DependencyResolver\SolverProblemsException; use Composer\Package\Link; +use Composer\Repository\RepositorySet; use Composer\TestCase; use Composer\Semver\Constraint\MultiConstraint; class SolverTest extends TestCase { - protected $pool; + protected $repoSet; protected $repo; protected $repoInstalled; protected $request; @@ -33,13 +34,13 @@ class SolverTest extends TestCase public function setUp() { - $this->pool = new Pool; + $this->repoSet = new RepositorySet(new Pool); $this->repo = new ArrayRepository; $this->repoInstalled = new ArrayRepository; - $this->request = new Request($this->pool); + $this->request = new Request($this->repoSet); $this->policy = new DefaultPolicy; - $this->solver = new Solver($this->policy, $this->pool, $this->repoInstalled, new NullIO()); + $this->solver = new Solver($this->policy, $this->repoSet, $this->repoInstalled, new NullIO()); } public function testSolverInstallSingle() @@ -90,9 +91,9 @@ class SolverTest extends TestCase $repo1->addPackage($foo1 = $this->getPackage('foo', '1')); $repo2->addPackage($foo2 = $this->getPackage('foo', '1')); - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($repo1); - $this->pool->addRepository($repo2); + $this->repoSet->addRepository($this->repoInstalled); + $this->repoSet->addRepository($repo1); + $this->repoSet->addRepository($repo2); $this->request->install('foo'); @@ -839,8 +840,8 @@ class SolverTest extends TestCase protected function reposComplete() { - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($this->repo); + $this->repoSet->addRepository($this->repoInstalled); + $this->repoSet->addRepository($this->repo); } protected function checkSolverResult(array $expected) diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 7f0327d9c..54460d705 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -411,12 +411,12 @@ class EventDispatcherTest extends TestCase ->will($this->returnValue(array())); $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); - $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); + $repositorySet = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); $installedRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); - $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, true, $policy, $pool, $installedRepo, $request); - $dispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, true, $policy, $pool, $installedRepo, $request, array()); + $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, true, $policy, $repositorySet, $installedRepo, $request); + $dispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, true, $policy, $repositorySet, $installedRepo, $request, array()); } public static function call() diff --git a/tests/Composer/Test/Installer/InstallerEventTest.php b/tests/Composer/Test/Installer/InstallerEventTest.php index 8c99ba565..2847432e3 100644 --- a/tests/Composer/Test/Installer/InstallerEventTest.php +++ b/tests/Composer/Test/Installer/InstallerEventTest.php @@ -22,18 +22,18 @@ class InstallerEventTest extends TestCase $composer = $this->getMockBuilder('Composer\Composer')->getMock(); $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); - $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); + $repositorySet = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); $installedRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); $operations = array($this->getMockBuilder('Composer\DependencyResolver\Operation\OperationInterface')->getMock()); - $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, $policy, $pool, $installedRepo, $request, $operations); + $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, $policy, $repositorySet, $installedRepo, $request, $operations); $this->assertSame('EVENT_NAME', $event->getName()); $this->assertInstanceOf('Composer\Composer', $event->getComposer()); $this->assertInstanceOf('Composer\IO\IOInterface', $event->getIO()); $this->assertTrue($event->isDevMode()); $this->assertInstanceOf('Composer\DependencyResolver\PolicyInterface', $event->getPolicy()); - $this->assertInstanceOf('Composer\DependencyResolver\Pool', $event->getPool()); + $this->assertInstanceOf('Composer\Repository\RepositorySet', $event->getRepositorySet()); $this->assertInstanceOf('Composer\Repository\CompositeRepository', $event->getInstalledRepo()); $this->assertInstanceOf('Composer\DependencyResolver\Request', $event->getRequest()); $this->assertCount(1, $event->getOperations()); diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php index d3e831e5c..2617eecf2 100644 --- a/tests/Composer/Test/Package/Version/VersionSelectorTest.php +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -21,7 +21,7 @@ use PHPUnit\Framework\TestCase; class VersionSelectorTest extends TestCase { // A) multiple versions, get the latest one - // B) targetPackageVersion will pass to pool + // B) targetPackageVersion will pass to repo set // C) No results, throw exception public function testLatestVersionIsReturned() @@ -33,13 +33,13 @@ class VersionSelectorTest extends TestCase $package3 = $this->createPackage('1.2.0'); $packages = array($package1, $package2, $package3); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName); // 1.2.2 should be returned because it's the latest of the returned versions @@ -57,13 +57,13 @@ class VersionSelectorTest extends TestCase $package2->setRequires(array('php' => new Link($packageName, 'php', $parser->parseConstraints('>=5.6'), 'requires', '>=5.6'))); $packages = array($package1, $package2); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, '5.5.0'); $this->assertSame($package1, $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); @@ -77,13 +77,13 @@ class VersionSelectorTest extends TestCase $package2 = $this->createPackage('1.1.0-beta'); $packages = array($package1, $package2); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName); $this->assertSame($package1, $best, 'Latest most stable version should be returned (1.0.0)'); @@ -97,18 +97,18 @@ class VersionSelectorTest extends TestCase $package2 = $this->createPackage('2.0.0-beta3'); $packages = array($package1, $package2); - $pool = $this->createMockPool(); - $pool->expects($this->at(0)) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->at(0)) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $pool->expects($this->at(1)) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet->expects($this->at(1)) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue(array_reverse($packages))); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, null); $this->assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); @@ -124,13 +124,13 @@ class VersionSelectorTest extends TestCase $package2 = $this->createPackage('1.1.0-beta'); $packages = array($package1, $package2); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, null, 'dev'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); @@ -145,13 +145,13 @@ class VersionSelectorTest extends TestCase $package3 = $this->createPackage('1.2.0-alpha'); $packages = array($package1, $package2, $package3); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, null, 'beta'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); @@ -165,13 +165,13 @@ class VersionSelectorTest extends TestCase $package3 = $this->createPackage('1.2.0-alpha'); $packages = array($package2, $package3); - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') - ->with($packageName, null, true) + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') + ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, null, 'stable'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); @@ -179,12 +179,12 @@ class VersionSelectorTest extends TestCase public function testFalseReturnedOnNoPackages() { - $pool = $this->createMockPool(); - $pool->expects($this->once()) - ->method('whatProvides') + $repositorySet = $this->createMockRepositorySet(); + $repositorySet->expects($this->once()) + ->method('findPackages') ->will($this->returnValue(array())); - $versionSelector = new VersionSelector($pool); + $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate('foobaz'); $this->assertFalse($best, 'No versions are available returns false'); } @@ -194,8 +194,8 @@ class VersionSelectorTest extends TestCase */ public function testFindRecommendedRequireVersion($prettyVersion, $isDev, $stability, $expectedVersion, $branchAlias = null) { - $pool = $this->createMockPool(); - $versionSelector = new VersionSelector($pool); + $repositorySet = $this->createMockRepositorySet(); + $versionSelector = new VersionSelector($repositorySet); $versionParser = new VersionParser(); $package = $this->getMockBuilder('\Composer\Package\PackageInterface')->getMock(); @@ -273,8 +273,10 @@ class VersionSelectorTest extends TestCase return new Package('foo', $parser->normalize($version), $version); } - private function createMockPool() + private function createMockRepositorySet() { - return $this->getMockBuilder('Composer\DependencyResolver\Pool')->getMock(); + return $this->getMockBuilder('Composer\Repository\RepositorySet') + ->disableOriginalConstructor() + ->getMock(); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json index 574c4402f..335f772c9 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json index 27432acfa..4104f4be6 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin2" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json index 881eb5cae..ee087e2d7 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin2" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json index f61cb3fbd..a349ccc2c 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json @@ -10,6 +10,6 @@ ] }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v8/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v8/composer.json index 799df2e61..aa44b5a3d 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v8/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v8/composer.json @@ -9,6 +9,6 @@ ] }, "require": { - "composer-plugin-api": "1.1.0" + "composer-plugin-api": "2.0.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v9/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v9/composer.json index f3ccb9397..45d8d794b 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v9/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v9/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^2.0" } } From 1228bcdffc2fca0806b16dfb9d148ffa9d845383 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 11 Sep 2018 13:33:29 +0200 Subject: [PATCH 003/321] Internalize pool creation in repository set, store root aliases in set The pool is still exposed too early in a few places which will require further refactoring --- .../Command/BaseDependencyCommand.php | 3 +- src/Composer/Command/CreateProjectCommand.php | 3 +- src/Composer/Command/InitCommand.php | 3 +- src/Composer/Command/ShowCommand.php | 5 +- src/Composer/DependencyResolver/Pool.php | 18 +-- src/Composer/Installer.php | 36 +++--- src/Composer/Plugin/PluginManager.php | 3 +- .../Repository/ComposerRepository.php | 15 ++- src/Composer/Repository/RepositorySet.php | 112 +++++++++++++++-- .../DependencyResolver/DefaultPolicyTest.php | 117 +++++++++++------- .../Test/DependencyResolver/PoolTest.php | 21 ++-- .../RuleSetIteratorTest.php | 3 +- .../Test/DependencyResolver/RuleSetTest.php | 3 +- .../Test/DependencyResolver/RuleTest.php | 3 +- .../Test/DependencyResolver/SolverTest.php | 2 +- .../Test/Plugin/PluginInstallerTest.php | 22 ++-- .../Repository/ComposerRepositoryTest.php | 12 +- 17 files changed, 248 insertions(+), 133 deletions(-) diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index ff7bae2fe..ca80fa246 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -12,7 +12,6 @@ namespace Composer\Command; -use Composer\DependencyResolver\Pool; use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Repository\ArrayRepository; @@ -79,7 +78,7 @@ class BaseDependencyCommand extends BaseCommand $composer->getRepositoryManager()->getLocalRepository(), new PlatformRepository(array(), $platformOverrides), )); - $repositorySet = new RepositorySet(new Pool()); + $repositorySet = new RepositorySet(); $repositorySet->addRepository($repository); // Parse package name and constraint diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 4c5a21794..1b58d59c5 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -20,7 +20,6 @@ use Composer\Installer\InstallationManager; use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\BasePackage; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\Package\Version\VersionSelector; use Composer\Package\AliasPackage; @@ -291,7 +290,7 @@ EOT throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } - $repositorySet = new RepositorySet(new Pool($stability)); + $repositorySet = new RepositorySet(array(), $stability); $repositorySet->addRepository($sourceRepo); $phpVersion = null; diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 86c55b152..03504e29a 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -12,7 +12,6 @@ namespace Composer\Command; -use Composer\DependencyResolver\Pool; use Composer\Factory; use Composer\Json\JsonFile; use Composer\Package\BasePackage; @@ -643,7 +642,7 @@ EOT $key = $minimumStability ?: 'default'; if (!isset($this->repositorySets[$key])) { - $this->repositorySets[$key] = $repositorySet = new RepositorySet(new Pool($minimumStability ?: $this->getMinimumStability($input))); + $this->repositorySets[$key] = $repositorySet = new RepositorySet(array(), $minimumStability ?: $this->getMinimumStability($input)); $repositorySet->addRepository($this->getRepos()); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index edb7198c9..94ec78362 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -14,7 +14,6 @@ namespace Composer\Command; use Composer\Composer; use Composer\DependencyResolver\DefaultPolicy; -use Composer\DependencyResolver\Pool; use Composer\Json\JsonFile; use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; @@ -524,7 +523,7 @@ EOT $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version; $policy = new DefaultPolicy(); - $repositorySet = new RepositorySet(new Pool('dev')); + $repositorySet = new RepositorySet(array(), 'dev'); $repositorySet->addRepository($repos); $matchedPackage = null; @@ -985,7 +984,7 @@ EOT private function getRepositorySet(Composer $composer) { if (!$this->repositorySet) { - $this->repositorySet = new RepositorySet(new Pool($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags())); + $this->repositorySet = new RepositorySet(array(), $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index c63556974..ee4bace97 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -54,22 +54,12 @@ class Pool implements \Countable protected $whitelist = null; protected $id = 1; - public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) + public function __construct(array $acceptableStabilities, array $stabilityFlags = array(), array $filterRequires = array()) { - $this->versionParser = new VersionParser; - $this->acceptableStabilities = array(); - foreach (BasePackage::$stabilities as $stability => $value) { - if ($value <= BasePackage::$stabilities[$minimumStability]) { - $this->acceptableStabilities[$stability] = $value; - } - } + $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; $this->filterRequires = $filterRequires; - foreach ($filterRequires as $name => $constraint) { - if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) { - unset($this->filterRequires[$name]); - } - } + $this->versionParser = new VersionParser; } public function setWhitelist($whitelist) @@ -202,7 +192,7 @@ class Pool implements \Countable $candidates = array(); foreach ($this->providerRepos as $repo) { - foreach ($repo->whatProvides($this, $name, $bypassFilters) as $candidate) { + foreach ($repo->whatProvides($name, $bypassFilters, array($this, 'isPackageAcceptable')) as $candidate) { $candidates[] = $candidate; if ($candidate->id < 1) { $candidate->setId($this->id++); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 73120ea4b..bb27bda3c 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -20,7 +20,6 @@ use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\PolicyInterface; -use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\Solver; @@ -371,19 +370,19 @@ class Installer // creating repository set $policy = $this->createPolicy(); - $repositorySet = $this->createRepositorySet($this->update ? null : $lockedRepository); - $repositorySet->addRepository($installedRepo, $aliases); + $repositorySet = $this->createRepositorySet($aliases, $this->update ? null : $lockedRepository); + $repositorySet->addRepository($installedRepo); if ($this->update) { $repositories = $this->repositoryManager->getRepositories(); foreach ($repositories as $repository) { - $repositorySet->addRepository($repository, $aliases); + $repositorySet->addRepository($repository); } } // Add the locked repository after the others in case we are doing a // partial update so missing packages can be found there still. // For installs from lock it's the only one added so it is first if ($lockedRepository) { - $repositorySet->addRepository($lockedRepository, $aliases); + $repositorySet->addRepository($lockedRepository); } // creating requirements request @@ -465,6 +464,8 @@ class Installer } } + $repositorySet->getPoolTemp(); // TODO remove this, but ensures ids are set before dev packages are processed in advance of solver + // force dev packages to have the latest links if we update or install from a (potentially new) lock $this->processDevPackages($localRepo, $repositorySet, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); @@ -685,9 +686,9 @@ class Installer unset($tempLocalRepo, $loader, $dumper); $policy = $this->createPolicy(); - $repositorySet = $this->createRepositorySet(); + $repositorySet = $this->createRepositorySet($aliases); $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); - $repositorySet->addRepository($installedRepo, $aliases); + $repositorySet->addRepository($installedRepo); // creating requirements request without dev requirements $request = $this->createRequest($this->package, $platformRepo); @@ -844,17 +845,12 @@ class Installer return $installedRepo; } - private function createRepositorySet($lockedRepository = null) - { - $pool = $this->createPool($lockedRepository); - return new RepositorySet($pool); - } - /** - * @param RepositoryInterface|null $lockedRepository - * @return Pool + * @param array $rootAliases + * @param RepositoryInterface|null $lockedRepository + * @return RepositorySet */ - private function createPool(RepositoryInterface $lockedRepository = null) + private function createRepositorySet(array $rootAliases = array(), $lockedRepository = null) { if ($this->update) { $minimumStability = $this->package->getMinimumStability(); @@ -886,7 +882,7 @@ class Installer } } - return new Pool($minimumStability, $stabilityFlags, $rootConstraints); + return new RepositorySet($rootAliases, $minimumStability, $stabilityFlags, $rootConstraints); } /** @@ -1319,7 +1315,7 @@ class Installer } } - $repositorySet = new RepositorySet(new Pool('dev')); + $repositorySet = new RepositorySet(array(), 'dev'); $repositorySet->addRepository($localOrLockRepo); $seen = array(); @@ -1354,11 +1350,11 @@ class Installer while (!$packageQueue->isEmpty()) { $package = $packageQueue->dequeue(); - if (isset($seen[$package->getId()])) { + if (isset($seen[spl_object_hash($package)])) { continue; } - $seen[$package->getId()] = true; + $seen[spl_object_hash($package)] = true; $this->updateWhitelist[$package->getName()] = true; if (!$this->whitelistDependencies && !$this->whitelistAllDependencies) { diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index c6bdefc5a..d16c51db1 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -23,7 +23,6 @@ use Composer\Package\PackageInterface; use Composer\Package\Link; use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; -use Composer\DependencyResolver\Pool; use Composer\Plugin\Capability\Capability; /** @@ -158,7 +157,7 @@ class PluginManager $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $repositorySet = new RepositorySet(new Pool('dev')); + $repositorySet = new RepositorySet(array(), 'dev'); $repositorySet->addRepository($localRepo); if ($globalRepo) { $repositorySet->addRepository($globalRepo); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 8a5da2b23..cec2ae948 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -16,7 +16,6 @@ use Composer\Package\Loader\ArrayLoader; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; -use Composer\DependencyResolver\Pool; use Composer\Json\JsonFile; use Composer\Cache; use Composer\Config; @@ -136,7 +135,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - $packages = $this->whatProvides(new Pool('dev'), $providerName); + $packages = $this->whatProvides($providerName); foreach ($packages as $package) { if ($name === $package->getName()) { $pkgConstraint = new Constraint('==', $package->getVersion()); @@ -170,7 +169,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - $candidates = $this->whatProvides(new Pool('dev'), $providerName); + $candidates = $this->whatProvides($providerName); // TODO what is the point of this? foreach ($candidates as $package) { if ($name === $package->getName()) { $pkgConstraint = new Constraint('==', $package->getVersion()); @@ -289,12 +288,12 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } /** - * @param Pool $pool - * @param string $name package name - * @param bool $bypassFilters If set to true, this bypasses the stability filtering, and forces a recompute without cache + * @param string $name package name + * @param bool $bypassFilters If set to true, this bypasses the stability filtering, and forces a recompute without cache + * @param callable $isPackageAcceptableCallable * @return array|mixed */ - public function whatProvides(Pool $pool, $name, $bypassFilters = false) + public function whatProvides($name, $bypassFilters = false, $isPackageAcceptableCallable = null) { if (isset($this->providers[$name]) && !$bypassFilters) { return $this->providers[$name]; @@ -395,7 +394,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } } else { - if (!$bypassFilters && !$pool->isPackageAcceptable(strtolower($version['name']), VersionParser::parseStability($version['version']))) { + if (!$bypassFilters && (!$isPackageAcceptableCallable || !call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version'])))) { continue; } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index e6b6db26b..d3c652ccb 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -24,36 +24,132 @@ use Composer\Semver\Constraint\ConstraintInterface; */ class RepositorySet { - private $pool; + /** @var array */ + private $rootAliases; - public function __construct(Pool $pool) + /** @var RepositoryInterface[] */ + private $repositories; + + /** @var ComposerRepository[] */ + private $providerRepos; + + private $acceptableStabilities; + private $stabilityFlags; + protected $filterRequires; + + /** @var Pool */ + private $pool; // TODO remove this + + public function __construct(array $rootAliases = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) { - $this->pool = $pool; + $this->rootAliases = $rootAliases; + + $this->acceptableStabilities = array(); + foreach (BasePackage::$stabilities as $stability => $value) { + if ($value <= BasePackage::$stabilities[$minimumStability]) { + $this->acceptableStabilities[$stability] = $value; + } + } + $this->stabilityFlags = $stabilityFlags; + $this->filterRequires = $filterRequires; + foreach ($filterRequires as $name => $constraint) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) { + unset($this->filterRequires[$name]); + } + } } - public function addRepository(RepositoryInterface $repo, $rootAliases = array()) + /** + * Adds a repository to this repository set + * + * @param RepositoryInterface $repo A package repository + */ + public function addRepository(RepositoryInterface $repo) { - return $this->pool->addRepository($repo, $rootAliases); + if ($repo instanceof CompositeRepository) { + $repos = $repo->getRepositories(); + } else { + $repos = array($repo); + } + + foreach ($repos as $repo) { + $this->repositories[] = $repo; + if ($repo instanceof ComposerRepository && $repo->hasProviders()) { + $this->providerRepos[] = $repo; + } + } } public function isPackageAcceptable($name, $stability) { - return $this->pool->isPackageAcceptable($name, $stability); + foreach ((array) $name as $n) { + // allow if package matches the global stability requirement and has no exception + if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) { + return true; + } + + // allow if package matches the package-specific stability flag + if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) { + return true; + } + } + + return false; } + /** + * Find packages matching name and optionally a constraint in all repositories + * + * @param $name + * @param ConstraintInterface|null $constraint + * @return array + */ public function findPackages($name, ConstraintInterface $constraint = null) { - return $this->pool->whatProvides($name, $constraint, true); + $packages = array(); + foreach ($this->repositories as $repository) { + $packages[] = $repository->findPackages($name, $constraint) ?: array(); + } + + $candidates = $packages ? call_user_func_array('array_merge', $packages) : array(); + + $result = array(); + foreach ($candidates as $candidate) { + if ($this->isPackageAcceptable($candidate->getNames(), $candidate->getStability())) { + $result[] = $candidate; + } + } + + return $candidates; } + /** + * Create a pool for dependency resolution from the packages in this repository set. + * + * @return Pool + */ public function createPool() { + if ($this->pool) { + return $this->pool; + } + + $this->pool = new Pool($this->acceptableStabilities, $this->stabilityFlags, $this->filterRequires); + + foreach ($this->repositories as $repository) { + $this->pool->addRepository($repository, $this->rootAliases); + } + return $this->pool; } // TODO get rid of this function public function getPoolTemp() { - return $this->pool; + if (!$this->pool) { + return $this->createPool(); + } else { + return $this->pool; + } } } diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index a73139d54..34a1c092b 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -18,13 +18,14 @@ use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\Pool; use Composer\Package\Link; use Composer\Package\AliasPackage; +use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\TestCase; class DefaultPolicyTest extends TestCase { - /** @var Pool */ - protected $pool; + /** @var RepositorySet */ + protected $repositorySet; /** @var ArrayRepository */ protected $repo; /** @var ArrayRepository */ @@ -34,7 +35,7 @@ class DefaultPolicyTest extends TestCase public function setUp() { - $this->pool = new Pool('dev'); + $this->repositorySet = new RepositorySet(array(), 'dev'); $this->repo = new ArrayRepository; $this->repoInstalled = new ArrayRepository; @@ -44,12 +45,14 @@ class DefaultPolicyTest extends TestCase public function testSelectSingle() { $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA->getId()); $expected = array($packageA->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -58,12 +61,14 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -72,12 +77,14 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0.0')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -86,13 +93,15 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0.0')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); $policy = new DefaultPolicy(true); - $selected = $policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -101,12 +110,14 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA1 = $this->getPackage('A', 'dev-foo')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.0')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -115,13 +126,15 @@ class DefaultPolicyTest extends TestCase { $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); $this->repoInstalled->addPackage($packageAInstalled = $this->getPackage('A', '1.0')); - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoInstalled); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA->getId(), $packageAInstalled->getId()); $expected = array($packageA->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, $this->mapFromRepo($this->repoInstalled), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $this->mapFromRepo($this->repoInstalled), $literals); $this->assertSame($expected, $selected); } @@ -133,14 +146,16 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $otherRepository->addPackage($packageAImportant = $this->getPackage('A', '1.0')); - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($otherRepository); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoInstalled); + $this->repositorySet->addRepository($otherRepository); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA->getId(), $packageAImportant->getId()); $expected = array($packageAImportant->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -155,21 +170,25 @@ class DefaultPolicyTest extends TestCase $repo2->addPackage($package3 = $this->getPackage('A', '1.1')); $repo2->addPackage($package4 = $this->getPackage('A', '1.2')); - $this->pool->addRepository($repo1); - $this->pool->addRepository($repo2); + $this->repositorySet->addRepository($repo1); + $this->repositorySet->addRepository($repo2); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($package1->getId(), $package2->getId(), $package3->getId(), $package4->getId()); $expected = array($package2->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); - $this->pool = new Pool('dev'); - $this->pool->addRepository($repo2); - $this->pool->addRepository($repo1); + $this->repositorySet = new RepositorySet(array(), 'dev'); + $this->repositorySet->addRepository($repo2); + $this->repositorySet->addRepository($repo1); + + $pool = $this->repositorySet->getPoolTemp(); $expected = array($package4->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -186,11 +205,13 @@ class DefaultPolicyTest extends TestCase $repoImportant->addPackage($packageA2AliasImportant = new AliasPackage($packageA2Important, '2.1.9999999.9999999-dev', '2.1.x-dev')); $packageAAliasImportant->setRootPackageAlias(true); - $this->pool->addRepository($this->repoInstalled); - $this->pool->addRepository($repoImportant); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoInstalled); + $this->repositorySet->addRepository($repoImportant); + $this->repositorySet->addRepository($this->repo); - $packages = $this->pool->whatProvides('a', new Constraint('=', '2.1.9999999.9999999-dev')); + $pool = $this->repositorySet->getPoolTemp(); + + $packages = $pool->whatProvides('a', new Constraint('=', '2.1.9999999.9999999-dev')); $literals = array(); foreach ($packages as $package) { $literals[] = $package->getId(); @@ -198,7 +219,7 @@ class DefaultPolicyTest extends TestCase $expected = array($packageAAliasImportant->getId()); - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -211,12 +232,14 @@ class DefaultPolicyTest extends TestCase $packageA->setProvides(array(new Link('A', 'X', new Constraint('==', '1.0'), 'provides'))); $packageB->setProvides(array(new Link('B', 'X', new Constraint('==', '1.0'), 'provides'))); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -228,12 +251,14 @@ class DefaultPolicyTest extends TestCase $packageB->setReplaces(array(new Link('B', 'A', new Constraint('==', '1.0'), 'replaces'))); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } @@ -247,12 +272,14 @@ class DefaultPolicyTest extends TestCase $packageA->setReplaces(array(new Link('vendor-a/replacer', 'vendor-a/package', new Constraint('==', '1.0'), 'replaces'))); $packageB->setReplaces(array(new Link('vendor-b/replacer', 'vendor-a/package', new Constraint('==', '1.0'), 'replaces'))); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals, 'vendor-a/package'); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals, 'vendor-a/package'); $this->assertEquals($expected, $selected); // test with reversed order in repo @@ -260,13 +287,15 @@ class DefaultPolicyTest extends TestCase $repo->addPackage($packageA = clone $packageA); $repo->addPackage($packageB = clone $packageB); - $pool = new Pool('dev'); - $pool->addRepository($this->repo); + $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($this->pool, array(), $literals, 'vendor-a/package'); + $selected = $this->policy->selectPreferredPackages($pool, array(), $literals, 'vendor-a/package'); $this->assertSame($expected, $selected); } @@ -286,12 +315,14 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); - $this->pool->addRepository($this->repo); + $this->repositorySet->addRepository($this->repo); + + $pool = $this->repositorySet->getPoolTemp(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); - $selected = $policy->selectPreferredPackages($this->pool, array(), $literals); + $selected = $policy->selectPreferredPackages($pool, array(), $literals); $this->assertSame($expected, $selected); } diff --git a/tests/Composer/Test/DependencyResolver/PoolTest.php b/tests/Composer/Test/DependencyResolver/PoolTest.php index 14b24fc9f..5169586f6 100644 --- a/tests/Composer/Test/DependencyResolver/PoolTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolTest.php @@ -21,7 +21,7 @@ class PoolTest extends TestCase { public function testPool() { - $pool = new Pool; + $pool = $this->createPool(); $repo = new ArrayRepository; $package = $this->getPackage('foo', '1'); @@ -34,7 +34,7 @@ class PoolTest extends TestCase public function testPoolIgnoresIrrelevantPackages() { - $pool = new Pool('stable', array('bar' => BasePackage::STABILITY_BETA)); + $pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE), array('bar' => BasePackage::STABILITY_BETA)); $repo = new ArrayRepository; $repo->addPackage($package = $this->getPackage('bar', '1')); $repo->addPackage($betaPackage = $this->getPackage('bar', '1-beta')); @@ -53,7 +53,7 @@ class PoolTest extends TestCase */ public function testGetPriorityForNotRegisteredRepository() { - $pool = new Pool; + $pool = $this->createPool(); $repository = new ArrayRepository; $pool->getPriority($repository); @@ -61,7 +61,7 @@ class PoolTest extends TestCase public function testGetPriorityWhenRepositoryIsRegistered() { - $pool = new Pool; + $pool = $this->createPool(); $firstRepository = new ArrayRepository; $pool->addRepository($firstRepository); $secondRepository = new ArrayRepository; @@ -76,7 +76,7 @@ class PoolTest extends TestCase public function testWhatProvidesSamePackageForDifferentRepositories() { - $pool = new Pool; + $pool = $this->createPool(); $firstRepository = new ArrayRepository; $secondRepository = new ArrayRepository; @@ -96,7 +96,7 @@ class PoolTest extends TestCase public function testWhatProvidesPackageWithConstraint() { - $pool = new Pool; + $pool = $this->createPool(); $repository = new ArrayRepository; $firstPackage = $this->getPackage('foo', '1'); @@ -113,7 +113,7 @@ class PoolTest extends TestCase public function testPackageById() { - $pool = new Pool; + $pool = $this->createPool(); $repository = new ArrayRepository; $package = $this->getPackage('foo', '1'); @@ -125,8 +125,13 @@ class PoolTest extends TestCase public function testWhatProvidesWhenPackageCannotBeFound() { - $pool = new Pool; + $pool = $this->createPool(); $this->assertEquals(array(), $pool->whatProvides('foo')); } + + protected function createPool() + { + return new Pool(array('stable' => BasePackage::STABILITY_STABLE)); + } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index 7789881df..24a2e7c54 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -17,6 +17,7 @@ use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\RuleSetIterator; use Composer\DependencyResolver\Pool; +use Composer\Package\BasePackage; use PHPUnit\Framework\TestCase; class RuleSetIteratorTest extends TestCase @@ -26,7 +27,7 @@ class RuleSetIteratorTest extends TestCase protected function setUp() { - $this->pool = new Pool; + $this->pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); $this->rules = array( RuleSet::TYPE_JOB => array( diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index cecae613d..e9753c848 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -16,6 +16,7 @@ use Composer\DependencyResolver\GenericRule; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\Pool; +use Composer\Package\BasePackage; use Composer\Repository\ArrayRepository; use Composer\TestCase; @@ -25,7 +26,7 @@ class RuleSetTest extends TestCase public function setUp() { - $this->pool = new Pool; + $this->pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); } public function testAdd() diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index a0339f27a..c9b3dbe1a 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -16,6 +16,7 @@ use Composer\DependencyResolver\GenericRule; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\Pool; +use Composer\Package\BasePackage; use Composer\Repository\ArrayRepository; use Composer\TestCase; @@ -25,7 +26,7 @@ class RuleTest extends TestCase public function setUp() { - $this->pool = new Pool; + $this->pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); } public function testGetHash() diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 0142818fc..1a43c5b60 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -34,7 +34,7 @@ class SolverTest extends TestCase public function setUp() { - $this->repoSet = new RepositorySet(new Pool); + $this->repoSet = new RepositorySet(array()); $this->repo = new ArrayRepository; $this->repoInstalled = new ArrayRepository; diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index 26fc63efa..7981177eb 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -130,7 +130,7 @@ class PluginInstallerTest extends TestCase public function testInstallNewPlugin() { $this->repository - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('getPackages') ->will($this->returnValue(array())); $installer = new PluginInstaller($this->io, $this->composer); @@ -145,7 +145,7 @@ class PluginInstallerTest extends TestCase public function testInstallMultiplePlugins() { $this->repository - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('getPackages') ->will($this->returnValue(array($this->packages[3]))); $installer = new PluginInstaller($this->io, $this->composer); @@ -163,7 +163,7 @@ class PluginInstallerTest extends TestCase public function testUpgradeWithNewClassName() { $this->repository - ->expects($this->exactly(3)) + ->expects($this->once()) ->method('getPackages') ->will($this->returnValue(array($this->packages[0]))); $this->repository @@ -182,7 +182,7 @@ class PluginInstallerTest extends TestCase public function testUpgradeWithSameClassName() { $this->repository - ->expects($this->exactly(3)) + ->expects($this->once()) ->method('getPackages') ->will($this->returnValue(array($this->packages[1]))); $this->repository @@ -201,7 +201,7 @@ class PluginInstallerTest extends TestCase public function testRegisterPluginOnlyOneTime() { $this->repository - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('getPackages') ->will($this->returnValue(array())); $installer = new PluginInstaller($this->io, $this->composer); @@ -240,11 +240,11 @@ class PluginInstallerTest extends TestCase // Add the plugins to the repo along with the internal Plugin package on which they all rely. $this->repository - ->expects($this->any()) - ->method('getPackages') - ->will($this->returnCallback(function () use ($plugApiInternalPackage, $plugins) { - return array_merge(array($plugApiInternalPackage), $plugins); - })); + ->expects($this->any()) + ->method('getPackages') + ->will($this->returnCallback(function () use ($plugApiInternalPackage, $plugins) { + return array_merge(array($plugApiInternalPackage), $plugins); + })); $this->pm->loadInstalledPlugins(); } @@ -300,7 +300,7 @@ class PluginInstallerTest extends TestCase public function testCommandProviderCapability() { $this->repository - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('getPackages') ->will($this->returnValue(array($this->packages[7]))); $installer = new PluginInstaller($this->io, $this->composer); diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 38b459730..fa6809cbb 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -142,11 +142,6 @@ class ComposerRepositoryTest extends TestCase ), ))); - $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->getMock(); - $pool->expects($this->any()) - ->method('isPackageAcceptable') - ->will($this->returnValue(true)); - $versionParser = new VersionParser(); $repo->setRootAliases(array( 'a' => array( @@ -155,7 +150,7 @@ class ComposerRepositoryTest extends TestCase ), )); - $packages = $repo->whatProvides($pool, 'a'); + $packages = $repo->whatProvides('a', false, array($this, 'isPackageAcceptableReturnTrue')); $this->assertCount(7, $packages); $this->assertEquals(array('1', '1-alias', '2', '2-alias', '2-root', '3', '3-root'), array_keys($packages)); @@ -164,6 +159,11 @@ class ComposerRepositoryTest extends TestCase $this->assertSame($packages['2'], $packages['2-alias']->getAliasOf()); } + public function isPackageAcceptableReturnTrue() + { + return true; + } + public function testSearchWithType() { $repoConfig = array( From 190d263c74773e855ecc2d854766ec3963db448e Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 11 Sep 2018 14:40:37 +0200 Subject: [PATCH 004/321] Fix logic for composer repository's optional acceptable callable filter --- src/Composer/Repository/ComposerRepository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index cec2ae948..ea583ede6 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -169,7 +169,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - $candidates = $this->whatProvides($providerName); // TODO what is the point of this? + $candidates = $this->whatProvides($providerName); foreach ($candidates as $package) { if ($name === $package->getName()) { $pkgConstraint = new Constraint('==', $package->getVersion()); @@ -394,7 +394,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } } else { - if (!$bypassFilters && (!$isPackageAcceptableCallable || !call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version'])))) { + if (!$bypassFilters && $isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { continue; } From 7036f999990a4a2ec394cf0fb0506f860ecdd19b Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 11 Sep 2018 14:52:44 +0200 Subject: [PATCH 005/321] RepositorySet::findPackages now has an exactMatch option Allows search for providers/replacers, or exact name search --- src/Composer/Command/BaseDependencyCommand.php | 2 +- src/Composer/Plugin/PluginManager.php | 2 +- src/Composer/Repository/RepositorySet.php | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index ca80fa246..b2b617c42 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -89,7 +89,7 @@ class BaseDependencyCommand extends BaseCommand ); // Find packages that are or provide the requested package first - $packages = $repositorySet->findPackages(strtolower($needle)); // TODO this does not search providers + $packages = $repositorySet->findPackages(strtolower($needle), null, false); if (empty($packages)) { throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index d16c51db1..786d846c5 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -305,7 +305,7 @@ class PluginManager */ private function lookupInstalledPackage(RepositorySet $repositorySet, Link $link) { - $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint()); // TODO this no longer returns providers + $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint(), false); return !empty($packages) ? $packages[0] : null; } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index d3c652ccb..f83de3b14 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -98,13 +98,14 @@ class RepositorySet } /** - * Find packages matching name and optionally a constraint in all repositories + * Find packages providing or matching a name and optionally meeting a constraint in all repositories * - * @param $name + * @param string $name * @param ConstraintInterface|null $constraint + * @param bool $exactMatch * @return array */ - public function findPackages($name, ConstraintInterface $constraint = null) + public function findPackages($name, ConstraintInterface $constraint = null, $exactMatch = true) { $packages = array(); foreach ($this->repositories as $repository) { @@ -115,6 +116,10 @@ class RepositorySet $result = array(); foreach ($candidates as $candidate) { + if ($exactMatch && $candidate->getName() !== $name) { + continue; + } + if ($this->isPackageAcceptable($candidate->getNames(), $candidate->getStability())) { $result[] = $candidate; } From b6e2d60c9eee1a75ac5d1b8a1bb621ae4baf6b81 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 11 Sep 2018 15:49:08 +0200 Subject: [PATCH 006/321] Create the pool in the installer before giving it to the solver --- src/Composer/DependencyResolver/Solver.php | 14 +++---- src/Composer/Installer.php | 42 ++++++++++--------- src/Composer/Repository/RepositorySet.php | 28 ++++++------- .../DependencyResolver/DefaultPolicyTest.php | 30 ++++++------- .../Test/DependencyResolver/SolverTest.php | 5 ++- 5 files changed, 60 insertions(+), 59 deletions(-) diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index f5226fca5..959f1dc4c 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -27,8 +27,8 @@ class Solver /** @var PolicyInterface */ protected $policy; - /** @var RepositorySet */ - protected $repositorySet = null; + /** @var Pool */ + protected $pool = null; /** @var RepositoryInterface */ protected $installed; /** @var RuleSet */ @@ -37,8 +37,6 @@ class Solver protected $ruleSetGenerator; /** @var array */ protected $jobs; - /** @var Pool */ - protected $pool = null; /** @var int[] */ protected $updateMap = array(); @@ -65,15 +63,15 @@ class Solver /** * @param PolicyInterface $policy - * @param RepositorySet $repositorySet + * @param Pool $pool * @param RepositoryInterface $installed * @param IOInterface $io */ - public function __construct(PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $installed, IOInterface $io) + public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed, IOInterface $io) { $this->io = $io; $this->policy = $policy; - $this->repositorySet = $repositorySet; + $this->pool = $pool; $this->installed = $installed; } @@ -217,8 +215,6 @@ class Solver { $this->jobs = $request->getJobs(); - $this->pool = $this->repositorySet->createPool(); - $this->setupInstalledMap(); $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index bb27bda3c..c6f888493 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -20,6 +20,7 @@ use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\PolicyInterface; +use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\Solver; @@ -464,14 +465,15 @@ class Installer } } - $repositorySet->getPoolTemp(); // TODO remove this, but ensures ids are set before dev packages are processed in advance of solver + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); + + $pool = $repositorySet->createPool(); // force dev packages to have the latest links if we update or install from a (potentially new) lock - $this->processDevPackages($localRepo, $repositorySet, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); + $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); // solve dependencies - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); - $solver = new Solver($policy, $repositorySet, $installedRepo, $this->io); + $solver = new Solver($policy, $pool, $installedRepo, $this->io); try { $operations = $solver->solve($request, $this->ignorePlatformReqs); } catch (SolverProblemsException $e) { @@ -485,7 +487,7 @@ class Installer } // force dev packages to be updated if we update or install from a (potentially new) lock - $operations = $this->processDevPackages($localRepo, $repositorySet, $policy, $repositories, $installedRepo, $lockedRepository, 'force-updates', $operations); + $operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-updates', $operations); $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations); @@ -600,11 +602,11 @@ class Installer if ($reason instanceof Rule) { switch ($reason->getReason()) { case Rule::RULE_JOB_INSTALL: - $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($solver->getPool())); + $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); $this->io->writeError(''); break; case Rule::RULE_PACKAGE_REQUIRES: - $this->io->writeError(' REASON: '.$reason->getPrettyString($solver->getPool())); + $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); $this->io->writeError(''); break; } @@ -623,7 +625,7 @@ class Installer if ($this->executeOperations) { // force source/dist urls to be updated for all packages - $this->processPackageUrls($repositorySet, $policy, $localRepo, $repositories); + $this->processPackageUrls($pool, $policy, $localRepo, $repositories); $localRepo->write(); } @@ -699,7 +701,7 @@ class Installer // solve deps to see which get removed $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request); - $solver = new Solver($policy, $repositorySet, $installedRepo, $this->io); + $solver = new Solver($policy, $repositorySet->createPool(), $installedRepo, $this->io); $ops = $solver->solve($request, $this->ignorePlatformReqs); $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request, $ops); @@ -948,7 +950,7 @@ class Installer /** * @param WritableRepositoryInterface $localRepo - * @param RepositorySet $repositorySet + * @param Pool $pool * @param PolicyInterface $policy * @param array $repositories * @param RepositoryInterface $installedRepo @@ -957,7 +959,7 @@ class Installer * @param array|null $operations * @return array */ - private function processDevPackages($localRepo, $repositorySet, $policy, $repositories, $installedRepo, $lockedRepository, $task, array $operations = null) + private function processDevPackages($localRepo, Pool $pool, $policy, $repositories, $installedRepo, $lockedRepository, $task, array $operations = null) { if ($task === 'force-updates' && null === $operations) { throw new \InvalidArgumentException('Missing operations argument'); @@ -1012,7 +1014,7 @@ class Installer } // find similar packages (name/version) in all repositories - $matches = $repositorySet->findPackages($package->getName(), new Constraint('=', $package->getVersion())); + $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion()), true); foreach ($matches as $index => $match) { // skip local packages if (!in_array($match->getRepository(), $repositories, true)) { @@ -1024,8 +1026,8 @@ class Installer } // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($repositorySet->getPoolTemp(), array(), $matches)) { // TODO remove temp call - $newPackage = $repositorySet->getPoolTemp()->literalToPackage($matches[0]); + if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { + $newPackage = $pool->literalToPackage($matches[0]); if ($task === 'force-links' && $newPackage) { $package->setRequires($newPackage->getRequires()); @@ -1126,12 +1128,12 @@ class Installer } /** - * @param RepositorySet $repositorySet + * @param Pool $pool * @param PolicyInterface $policy * @param WritableRepositoryInterface $localRepo * @param array $repositories */ - private function processPackageUrls($repositorySet, $policy, $localRepo, $repositories) + private function processPackageUrls(Pool $pool, $policy, $localRepo, $repositories) { if (!$this->update) { return; @@ -1140,8 +1142,8 @@ class Installer $rootRefs = $this->package->getReferences(); foreach ($localRepo->getCanonicalPackages() as $package) { - // find similar packages (name/version) in all repositories - $matches = $repositorySet->findPackages($package->getName(), new Constraint('=', $package->getVersion())); + // find similar packages (name/version) in pool + $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion()), true); foreach ($matches as $index => $match) { // skip local packages if (!in_array($match->getRepository(), $repositories, true)) { @@ -1153,8 +1155,8 @@ class Installer } // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($repositorySet->getPoolTemp(), array(), $matches)) { // TODO get rid of pool - $newPackage = $repositorySet->getPoolTemp()->literalToPackage($matches[0]); + if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { + $newPackage = $pool->literalToPackage($matches[0]); // update the dist and source URLs $sourceUrl = $package->getSourceUrl(); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index f83de3b14..4bea054f9 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -18,6 +18,7 @@ use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Test\DependencyResolver\PoolTest; /** * @author Nils Adermann @@ -28,17 +29,17 @@ class RepositorySet private $rootAliases; /** @var RepositoryInterface[] */ - private $repositories; + private $repositories = array(); /** @var ComposerRepository[] */ - private $providerRepos; + private $providerRepos = array(); private $acceptableStabilities; private $stabilityFlags; protected $filterRequires; /** @var Pool */ - private $pool; // TODO remove this + private $pool; public function __construct(array $rootAliases = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) { @@ -66,6 +67,10 @@ class RepositorySet */ public function addRepository(RepositoryInterface $repo) { + if ($this->pool) { + throw new \RuntimeException("Pool has already been created from this repository set, it cannot be modified anymore."); + } + if ($repo instanceof CompositeRepository) { $repos = $repo->getRepositories(); } else { @@ -135,10 +140,6 @@ class RepositorySet */ public function createPool() { - if ($this->pool) { - return $this->pool; - } - $this->pool = new Pool($this->acceptableStabilities, $this->stabilityFlags, $this->filterRequires); foreach ($this->repositories as $repository) { @@ -148,13 +149,12 @@ class RepositorySet return $this->pool; } - // TODO get rid of this function - public function getPoolTemp() + /** + * Access the pool object after it has been created, relevant for plugins which need to read info from the pool + * @return Pool + */ + public function getPool() { - if (!$this->pool) { - return $this->createPool(); - } else { - return $this->pool; - } + return $this->pool; } } diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 34a1c092b..dedb452e2 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -47,7 +47,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA->getId()); $expected = array($packageA->getId()); @@ -63,7 +63,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -79,7 +79,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -95,7 +95,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); @@ -112,7 +112,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -129,7 +129,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repoInstalled); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA->getId(), $packageAInstalled->getId()); $expected = array($packageA->getId()); @@ -150,7 +150,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($otherRepository); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA->getId(), $packageAImportant->getId()); $expected = array($packageAImportant->getId()); @@ -173,7 +173,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($repo1); $this->repositorySet->addRepository($repo2); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($package1->getId(), $package2->getId(), $package3->getId(), $package4->getId()); $expected = array($package2->getId()); @@ -185,7 +185,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($repo2); $this->repositorySet->addRepository($repo1); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $expected = array($package4->getId()); $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); @@ -209,7 +209,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($repoImportant); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $packages = $pool->whatProvides('a', new Constraint('=', '2.1.9999999.9999999-dev')); $literals = array(); @@ -234,7 +234,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -253,7 +253,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -274,7 +274,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -290,7 +290,7 @@ class DefaultPolicyTest extends TestCase $repositorySet = new RepositorySet(array(), 'dev'); $repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -317,7 +317,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->getPoolTemp(); + $pool = $this->repositorySet->createPool(); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 1a43c5b60..e4b02968f 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -40,7 +40,6 @@ class SolverTest extends TestCase $this->request = new Request($this->repoSet); $this->policy = new DefaultPolicy; - $this->solver = new Solver($this->policy, $this->repoSet, $this->repoInstalled, new NullIO()); } public function testSolverInstallSingle() @@ -95,6 +94,8 @@ class SolverTest extends TestCase $this->repoSet->addRepository($repo1); $this->repoSet->addRepository($repo2); + $this->solver = new Solver($this->policy, $this->repoSet->createPool(), $this->repoInstalled, new NullIO()); + $this->request->install('foo'); $this->checkSolverResult(array( @@ -842,6 +843,8 @@ class SolverTest extends TestCase { $this->repoSet->addRepository($this->repoInstalled); $this->repoSet->addRepository($this->repo); + + $this->solver = new Solver($this->policy, $this->repoSet->createPool(), $this->repoInstalled, new NullIO()); } protected function checkSolverResult(array $expected) From 1747df97e756430c33a3722f194f7a804afbbf97 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 11 Sep 2018 15:59:02 +0200 Subject: [PATCH 007/321] Create pool in show command to use policy, remove todos --- src/Composer/Command/ShowCommand.php | 6 ++++-- src/Composer/Installer.php | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 94ec78362..43546fdad 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -539,9 +539,11 @@ EOT $matches[$index] = $package->getId(); } + $pool = $repositorySet->createPool(); + // select preferred package according to policy rules - if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($repositorySet->getPoolTemp(), array(), $matches)) { // TODO get rid of the pool call - $matchedPackage = $repositorySet->getPoolTemp()->literalToPackage($preferred[0]); + if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) { + $matchedPackage = $pool->literalToPackage($preferred[0]); } return array($matchedPackage, $versions); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index c6f888493..029e6e18e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -127,7 +127,7 @@ class Installer * @var array|null */ protected $updateWhitelist = null; - protected $whitelistDependencies = false; // TODO 2.0 rename to whitelistTransitiveDependencies + protected $whitelistTransitiveDependencies = false; protected $whitelistAllDependencies = false; /** @@ -1327,7 +1327,7 @@ class Installer foreach ($this->updateWhitelist as $packageName => $void) { $packageQueue = new \SplQueue; - $depPackages = $repositorySet->findPackages($packageName); // TODO does this need replacers/providers? + $depPackages = $repositorySet->findPackages($packageName, null, false); $nameMatchesRequiredPackage = in_array($packageName, $requiredPackageNames, true); @@ -1359,14 +1359,14 @@ class Installer $seen[spl_object_hash($package)] = true; $this->updateWhitelist[$package->getName()] = true; - if (!$this->whitelistDependencies && !$this->whitelistAllDependencies) { + if (!$this->whitelistTransitiveDependencies && !$this->whitelistAllDependencies) { continue; } $requires = $package->getRequires(); foreach ($requires as $require) { - $requirePackages = $repositorySet->findPackages($require->getTarget()); // TODO does this need replacers/providers? + $requirePackages = $repositorySet->findPackages($require->getTarget(), null, false); foreach ($requirePackages as $requirePackage) { if (isset($this->updateWhitelist[$requirePackage->getName()])) { @@ -1678,7 +1678,7 @@ class Installer */ public function setWhitelistTransitiveDependencies($updateTransitiveDependencies = true) { - $this->whitelistDependencies = (bool) $updateTransitiveDependencies; + $this->whitelistTransitiveDependencies = (bool) $updateTransitiveDependencies; return $this; } From 4c7d271a36ddc484b107a06f674b7eb932755334 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 11 Sep 2018 16:03:48 +0200 Subject: [PATCH 008/321] Remove deprecated function --- src/Composer/Installer.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 029e6e18e..8f6427a9a 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -1659,14 +1659,6 @@ class Installer return $this; } - /** - * @deprecated use setWhitelistTransitiveDependencies instead - */ - public function setWhitelistDependencies($updateDependencies = true) - { - return $this->setWhitelistTransitiveDependencies($updateDependencies); - } - /** * Should dependencies of whitelisted packages (but not direct dependencies) be updated? * From c0f19f6c573550916373fbaae7245558ffb75af6 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 12 Sep 2018 11:49:09 +0200 Subject: [PATCH 009/321] Move construction of pool from repo set into a pool builder Pool construction depends on the install request now, so only required packages get loaded, add some structure for future asynchronously loading composer repositories --- .../DependencyResolver/DefaultPolicy.php | 11 +- src/Composer/DependencyResolver/Pool.php | 117 ++------------ .../DependencyResolver/PoolBuilder.php | 147 ++++++++++++++++++ src/Composer/DependencyResolver/Problem.php | 4 +- src/Composer/DependencyResolver/Rule.php | 15 +- src/Composer/DependencyResolver/Solver.php | 2 +- .../SolverProblemsException.php | 6 +- src/Composer/Installer.php | 4 +- src/Composer/Package/Locker.php | 4 +- .../Repository/AsyncRepositoryInterface.php | 38 +++++ src/Composer/Repository/BaseRepository.php | 15 ++ .../Repository/LockArrayRepository.php | 25 +++ .../Repository/RepositoryInterface.php | 9 ++ src/Composer/Repository/RepositorySet.php | 41 +++-- .../DependencyResolver/DefaultPolicyTest.php | 31 ++-- .../Test/DependencyResolver/PoolTest.php | 79 +--------- .../Test/DependencyResolver/RuleSetTest.php | 17 +- .../Test/DependencyResolver/RuleTest.php | 20 +-- .../Test/DependencyResolver/SolverTest.php | 19 ++- tests/Composer/Test/InstallerTest.php | 1 - 20 files changed, 357 insertions(+), 248 deletions(-) create mode 100644 src/Composer/DependencyResolver/PoolBuilder.php create mode 100644 src/Composer/Repository/AsyncRepositoryInterface.php create mode 100644 src/Composer/Repository/LockArrayRepository.php diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 542c6e625..051bc7449 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -57,11 +57,6 @@ class DefaultPolicy implements PolicyInterface return $packages; } - public function getPriority(Pool $pool, PackageInterface $package) - { - return $pool->getPriority($package->getRepository()); - } - public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null) { $packages = $this->groupLiteralsByNamePreferInstalled($pool, $installedMap, $literals); @@ -168,7 +163,7 @@ class DefaultPolicy implements PolicyInterface return 1; } - return ($this->getPriority($pool, $a) > $this->getPriority($pool, $b)) ? -1 : 1; + return ($pool->getPriority($a->id) > $pool->getPriority($b->id)) ? -1 : 1; } /** @@ -236,10 +231,10 @@ class DefaultPolicy implements PolicyInterface } if (null === $priority) { - $priority = $this->getPriority($pool, $package); + $priority = $pool->getPriority($package->id); } - if ($this->getPriority($pool, $package) != $priority) { + if ($pool->getPriority($package->id) != $priority) { break; } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index ee4bace97..6ad4d9f31 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -27,7 +27,7 @@ use Composer\Repository\PlatformRepository; use Composer\Package\PackageInterface; /** - * A package pool contains repositories that provide packages. + * A package pool contains all packages for dependency resolution * * @author Nils Adermann * @author Jordi Boggiano @@ -41,23 +41,18 @@ class Pool implements \Countable const MATCH_REPLACE = 3; const MATCH_FILTERED = 4; - protected $repositories = array(); protected $providerRepos = array(); protected $packages = array(); protected $packageByName = array(); protected $packageByExactName = array(); - protected $acceptableStabilities; - protected $stabilityFlags; + protected $priorities = array(); protected $versionParser; protected $providerCache = array(); protected $filterRequires; protected $whitelist = null; - protected $id = 1; - public function __construct(array $acceptableStabilities, array $stabilityFlags = array(), array $filterRequires = array()) + public function __construct(array $filterRequires = array()) { - $this->acceptableStabilities = $acceptableStabilities; - $this->stabilityFlags = $stabilityFlags; $this->filterRequires = $filterRequires; $this->versionParser = new VersionParser; } @@ -68,76 +63,24 @@ class Pool implements \Countable $this->providerCache = array(); } - /** - * Adds a repository and its packages to this package pool - * - * @param RepositoryInterface $repo A package repository - * @param array $rootAliases - */ - public function addRepository(RepositoryInterface $repo, $rootAliases = array()) + public function setPackages(array $packages, array $priorities = array()) { - if ($repo instanceof CompositeRepository) { - $repos = $repo->getRepositories(); - } else { - $repos = array($repo); - } + $this->priorities = $priorities; + $this->packages = $packages; - foreach ($repos as $repo) { - $this->repositories[] = $repo; + foreach ($this->packages as $package) { + $names = $package->getNames(); + $this->packageByExactName[$package->getName()][$package->id] = $package; - $exempt = $repo instanceof PlatformRepository || $repo instanceof InstalledRepositoryInterface; - - if ($repo instanceof ComposerRepository && $repo->hasProviders()) { - $this->providerRepos[] = $repo; - $repo->setRootAliases($rootAliases); - $repo->resetPackageIds(); - } else { - foreach ($repo->getPackages() as $package) { - $names = $package->getNames(); - $stability = $package->getStability(); - if ($exempt || $this->isPackageAcceptable($names, $stability)) { - $package->setId($this->id++); - $this->packages[] = $package; - $this->packageByExactName[$package->getName()][$package->id] = $package; - - foreach ($names as $provided) { - $this->packageByName[$provided][] = $package; - } - - // handle root package aliases - $name = $package->getName(); - if (isset($rootAliases[$name][$package->getVersion()])) { - $alias = $rootAliases[$name][$package->getVersion()]; - if ($package instanceof AliasPackage) { - $package = $package->getAliasOf(); - } - $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); - $aliasPackage->setRootPackageAlias(true); - $aliasPackage->setId($this->id++); - - $package->getRepository()->addPackage($aliasPackage); - $this->packages[] = $aliasPackage; - $this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage; - - foreach ($aliasPackage->getNames() as $name) { - $this->packageByName[$name][] = $aliasPackage; - } - } - } - } + foreach ($names as $provided) { + $this->packageByName[$provided][] = $package; } } } - public function getPriority(RepositoryInterface $repo) + public function getPriority($id) { - $priority = array_search($repo, $this->repositories, true); - - if (false === $priority) { - throw new \RuntimeException("Could not determine repository priority. The repository was not registered in the pool."); - } - - return -$priority; + return $this->priorities[$id - 1]; } /** @@ -191,25 +134,12 @@ class Pool implements \Countable { $candidates = array(); - foreach ($this->providerRepos as $repo) { - foreach ($repo->whatProvides($name, $bypassFilters, array($this, 'isPackageAcceptable')) as $candidate) { - $candidates[] = $candidate; - if ($candidate->id < 1) { - $candidate->setId($this->id++); - $this->packages[$this->id - 2] = $candidate; - } - } - } - if ($mustMatchName) { - $candidates = array_filter($candidates, function ($candidate) use ($name) { - return $candidate->getName() == $name; - }); if (isset($this->packageByExactName[$name])) { - $candidates = array_merge($candidates, $this->packageByExactName[$name]); + $candidates = $this->packageByExactName[$name]; } } elseif (isset($this->packageByName[$name])) { - $candidates = array_merge($candidates, $this->packageByName[$name]); + $candidates = $this->packageByName[$name]; } $matches = $provideMatches = array(); @@ -287,23 +217,6 @@ class Pool implements \Countable return $prefix.' '.$package->getPrettyString(); } - public function isPackageAcceptable($name, $stability) - { - foreach ((array) $name as $n) { - // allow if package matches the global stability requirement and has no exception - if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) { - return true; - } - - // allow if package matches the package-specific stability flag - if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) { - return true; - } - } - - return false; - } - /** * Checks if the package matches the given constraint directly or through * provided or replaced packages diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php new file mode 100644 index 000000000..2ec6dd680 --- /dev/null +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -0,0 +1,147 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\Package\AliasPackage; +use Composer\Package\BasePackage; +use Composer\Package\PackageInterface; +use Composer\Repository\AsyncRepositoryInterface; +use Composer\Repository\InstalledRepositoryInterface; +use Composer\Repository\LockArrayRepository; +use Composer\Repository\PlatformRepository; + +/** + * @author Nils Adermann + */ +class PoolBuilder +{ + private $isPackageAcceptableCallable; + private $filterRequires; + private $rootAliases; + + private $loadedNames = array(); + + private $id = 1; + private $packages = array(); + private $priorities = array(); + + public function __construct($isPackageAcceptableCallable, array $filterRequires = array()) + { + $this->isPackageAcceptableCallable = $isPackageAcceptableCallable; + $this->filterRequires = $filterRequires; + } + + public function buildPool(array $repositories, array $rootAliases, Request $request) + { + $this->pool = new Pool($this->filterRequires); + $this->rootAliases = $rootAliases; + + // TODO do we really want the request here? kind of want a root requirements thingy instead + $loadNames = array(); + foreach ($request->getJobs() as $job) { + switch ($job['cmd']) { + case 'install': + $loadNames[$job['packageName']] = true; + break; + } + } + + foreach ($repositories as $repository) { + if ($repository instanceof ComposerRepository && $repository->hasProviders()) { + $this->providerRepos[] = $repository; + $repository->setRootAliases($this->rootAliases); + $repository->resetPackageIds(); + } + } + + while (!empty($loadNames)) { + $loadIds = array(); + foreach ($repositories as $key => $repository) { + if ($repository instanceof AsyncRepositoryInterface) { + $loadIds[$key] = $repository->requestPackages($loadNames); + } + } + + foreach ($loadNames as $name => $void) { + $this->loadedNames[$name] = true; + } + + $newLoadNames = array(); + foreach ($repositories as $key => $repository) { + if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface) { + continue; + } + + if ($repository instanceof AsyncRepositoryInterface) { + $packages = $repository->returnPackages($loadIds[$key]); + } else { + $packages = $repository->loadPackages($loadNames); + } + + foreach ($packages as $package) { + if (call_user_func($this->isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { + $newLoadNames += $this->loadPackage($package, $key); + } + } + } + + $loadNames = $newLoadNames; + } + + foreach ($repositories as $key => $repository) { + if ($repository instanceof PlatformRepository || + $repository instanceof InstalledRepositoryInterface) { + foreach ($repository->getPackages() as $package) { + $this->loadPackage($package, $key); + } + } + } + + $this->pool->setPackages($this->packages, $this->priorities); + + return $this->pool; + } + + private function loadPackage(PackageInterface $package, $repoIndex) + { + $package->setId($this->id++); + $this->packages[] = $package; + $this->priorities[$this->id - 2] = -$repoIndex; + + // handle root package aliases + $name = $package->getName(); + if (isset($this->rootAliases[$name][$package->getVersion()])) { + $alias = $this->rootAliases[$name][$package->getVersion()]; + if ($package instanceof AliasPackage) { + $package = $package->getAliasOf(); + } + $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); + $aliasPackage->setRootPackageAlias(true); + $aliasPackage->setId($this->id++); + + $package->getRepository()->addPackage($aliasPackage); // TODO do we need this? + $this->packages[] = $aliasPackage; + } + + $loadNames = array(); + foreach ($package->getRequires() as $link) { + $require = $link->getTarget(); + if (!isset($this->loadedNames[$require])) { + $loadNames[$require] = true; + } + } + + return $loadNames; + } +} + diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index de24b0991..7cd3a9813 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -71,7 +71,7 @@ class Problem * @param array $installedMap A map of all installed packages * @return string */ - public function getPrettyString(array $installedMap = array()) + public function getPrettyString(array $installedMap = array(), array $learnedPool = array()) { $reasons = call_user_func_array('array_merge', array_reverse($this->reasons)); @@ -168,7 +168,7 @@ class Problem $messages[] = $this->jobToText($job); } elseif ($rule) { if ($rule instanceof Rule) { - $messages[] = $rule->getPrettyString($this->pool, $installedMap); + $messages[] = $rule->getPrettyString($this->pool, $installedMap, $learnedPool); } } } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 1fe8cbd30..a627de61c 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -126,7 +126,7 @@ abstract class Rule abstract public function isAssertion(); - public function getPrettyString(Pool $pool, array $installedMap = array()) + public function getPrettyString(Pool $pool, array $installedMap = array(), array $learnedPool = array()) { $literals = $this->getLiterals(); @@ -230,7 +230,18 @@ abstract class Rule case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: - return 'Conclusion: '.$ruleText; + // TODO not sure this is a good idea, most of these rules should be listed in the problem anyway + $learnedString = '(learned rule, '; + if (isset($learnedPool[$this->reasonData])) { + foreach ($learnedPool[$this->reasonData] as $learnedRule) { + $learnedString .= $learnedRule->getPrettyString($pool, $installedMap, $learnedPool); + } + } else { + $learnedString .= 'reasoning unavailable'; + } + $learnedString .= ')'; + + return 'Conclusion: '.$ruleText.' '.$learnedString; case self::RULE_PACKAGE_ALIAS: return $ruleText; default: diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 959f1dc4c..aa5432188 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -244,7 +244,7 @@ class Solver } if ($this->problems) { - throw new SolverProblemsException($this->problems, $this->installedMap); + throw new SolverProblemsException($this->problems, $this->installedMap, $this->learnedPool); } $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions); diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 142895697..0184bba9c 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -21,11 +21,13 @@ class SolverProblemsException extends \RuntimeException { protected $problems; protected $installedMap; + protected $learnedPool; - public function __construct(array $problems, array $installedMap) + public function __construct(array $problems, array $installedMap, array $learnedPool) { $this->problems = $problems; $this->installedMap = $installedMap; + $this->learnedPool = $learnedPool; parent::__construct($this->createMessage(), 2); } @@ -35,7 +37,7 @@ class SolverProblemsException extends \RuntimeException $text = "\n"; $hasExtensionProblems = false; foreach ($this->problems as $i => $problem) { - $text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap)."\n"; + $text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8f6427a9a..95d5b6605 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -467,7 +467,7 @@ class Installer $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); - $pool = $repositorySet->createPool(); + $pool = $repositorySet->createPool($request); // force dev packages to have the latest links if we update or install from a (potentially new) lock $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); @@ -701,7 +701,7 @@ class Installer // solve deps to see which get removed $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request); - $solver = new Solver($policy, $repositorySet->createPool(), $installedRepo, $this->io); + $solver = new Solver($policy, $repositorySet->createPool($request), $installedRepo, $this->io); $ops = $solver->solve($request, $this->ignorePlatformReqs); $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request, $ops); diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 57ec74233..405d43261 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -14,9 +14,9 @@ namespace Composer\Package; use Composer\Json\JsonFile; use Composer\Installer\InstallationManager; +use Composer\Repository\LockArrayRepository; use Composer\Repository\RepositoryManager; use Composer\Util\ProcessExecutor; -use Composer\Repository\ArrayRepository; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Loader\ArrayLoader; use Composer\Util\Git as GitUtil; @@ -150,7 +150,7 @@ class Locker public function getLockedRepository($withDevReqs = false) { $lockData = $this->getLockData(); - $packages = new ArrayRepository(); + $packages = new LockArrayRepository(); $lockedPackages = $lockData['packages']; if ($withDevReqs) { diff --git a/src/Composer/Repository/AsyncRepositoryInterface.php b/src/Composer/Repository/AsyncRepositoryInterface.php new file mode 100644 index 000000000..91f543dbf --- /dev/null +++ b/src/Composer/Repository/AsyncRepositoryInterface.php @@ -0,0 +1,38 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\Package\PackageInterface; + +/** + * Repository interface. + * + * @author Nils Adermann + * @author Konstantin Kudryashov + * @author Jordi Boggiano + */ +interface AsyncRepositoryInterface +{ + /** + * @param array $names Names of packages to retrieve data for + * @return scalar Id to be passed to later loadPackages call + */ + public function requestPackages(array $names); + + /** + * @param array $names + * @return scalar id for load call + */ + public function returnPackages($loadId); +} + diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/BaseRepository.php index 2b30b63cd..e9e97bb91 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/BaseRepository.php @@ -24,6 +24,21 @@ use Composer\Package\Link; */ abstract class BaseRepository implements RepositoryInterface { + // TODO should this stay here? some repos need a better implementation + public function loadPackages(array $packageNameMap) + { + $packages = $this->getPackages(); + + $result = array(); + foreach ($packages as $package) { + if (isset($packageNameMap[$package->getName()])) { + $result[] = $package; + } + } + + return $result; + } + /** * Returns a list of links causing the requested needle packages to be installed, as an associative array with the * dependent's name as key, and an array containing in order the PackageInterface and Link describing the relationship diff --git a/src/Composer/Repository/LockArrayRepository.php b/src/Composer/Repository/LockArrayRepository.php new file mode 100644 index 000000000..0ccc998d3 --- /dev/null +++ b/src/Composer/Repository/LockArrayRepository.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +/** + * Lock array repository. + * + * Regular array repository, only uses a different type to identify the lock file as the source of info + * + * @author Nils Adermann + */ +class LockArrayRepository extends ArrayRepository implements RepositoryInterface +{ +} + diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 9a2aaf3b5..703f92e35 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -55,6 +55,7 @@ interface RepositoryInterface extends \Countable */ public function findPackages($name, $constraint = null); + // TODO this should really not be in this generic interface anymore /** * Returns list of registered packages. * @@ -62,6 +63,14 @@ interface RepositoryInterface extends \Countable */ public function getPackages(); + /** + * Returns list of registered packages with the supplied name + * + * @param bool[] $packageNameMap + * @return PackageInterface[] + */ + public function loadPackages(array $packageNameMap); + /** * Searches the repository for packages containing the query * diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 4bea054f9..bac63dd36 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -13,6 +13,8 @@ namespace Composer\Repository; use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\PoolBuilder; +use Composer\DependencyResolver\Request; use Composer\Package\BasePackage; use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; @@ -31,9 +33,6 @@ class RepositorySet /** @var RepositoryInterface[] */ private $repositories = array(); - /** @var ComposerRepository[] */ - private $providerRepos = array(); - private $acceptableStabilities; private $stabilityFlags; protected $filterRequires; @@ -79,9 +78,6 @@ class RepositorySet foreach ($repos as $repo) { $this->repositories[] = $repo; - if ($repo instanceof ComposerRepository && $repo->hasProviders()) { - $this->providerRepos[] = $repo; - } } } @@ -133,20 +129,43 @@ class RepositorySet return $candidates; } + public function getPriority(RepositoryInterface $repo) + { + $priority = array_search($repo, $this->repositories, true); + + if (false === $priority) { + throw new \RuntimeException("Could not determine repository priority. The repository was not registered in the pool."); + } + + return -$priority; + } + /** * Create a pool for dependency resolution from the packages in this repository set. * * @return Pool */ - public function createPool() + public function createPool(Request $request) { - $this->pool = new Pool($this->acceptableStabilities, $this->stabilityFlags, $this->filterRequires); + $poolBuilder = new PoolBuilder(array($this, 'isPackageAcceptable'), $this->filterRequires); - foreach ($this->repositories as $repository) { - $this->pool->addRepository($repository, $this->rootAliases); + return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $request); + } + + // TODO unify this with above in some simpler version without "request"? + public function createPoolForPackage($packageName) + { + return $this->createPoolForPackages(array($packageName)); + } + + public function createPoolForPackages($packageNames) + { + $request = new Request(); + foreach ($packageNames as $packageName) { + $request->install($packageName); } - return $this->pool; + return $this->createPool($request); } /** diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index dedb452e2..c1637b8e4 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -21,6 +21,7 @@ use Composer\Package\AliasPackage; use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\TestCase; +use http\Env\Request; class DefaultPolicyTest extends TestCase { @@ -47,7 +48,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA->getId()); $expected = array($packageA->getId()); @@ -63,7 +64,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -79,7 +80,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -95,7 +96,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); @@ -112,7 +113,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -129,7 +130,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repoInstalled); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA->getId(), $packageAInstalled->getId()); $expected = array($packageA->getId()); @@ -150,7 +151,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($otherRepository); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA->getId(), $packageAImportant->getId()); $expected = array($packageAImportant->getId()); @@ -173,7 +174,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($repo1); $this->repositorySet->addRepository($repo2); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($package1->getId(), $package2->getId(), $package3->getId(), $package4->getId()); $expected = array($package2->getId()); @@ -185,7 +186,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($repo2); $this->repositorySet->addRepository($repo1); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $expected = array($package4->getId()); $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); @@ -209,7 +210,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($repoImportant); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $packages = $pool->whatProvides('a', new Constraint('=', '2.1.9999999.9999999-dev')); $literals = array(); @@ -234,7 +235,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackages(array('A', 'B')); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -253,7 +254,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackages(array('A', 'B')); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -274,7 +275,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer')); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -290,7 +291,7 @@ class DefaultPolicyTest extends TestCase $repositorySet = new RepositorySet(array(), 'dev'); $repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer')); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -317,7 +318,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPool(); + $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); diff --git a/tests/Composer/Test/DependencyResolver/PoolTest.php b/tests/Composer/Test/DependencyResolver/PoolTest.php index 5169586f6..74767c525 100644 --- a/tests/Composer/Test/DependencyResolver/PoolTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolTest.php @@ -22,90 +22,25 @@ class PoolTest extends TestCase public function testPool() { $pool = $this->createPool(); - $repo = new ArrayRepository; $package = $this->getPackage('foo', '1'); - $repo->addPackage($package); - $pool->addRepository($repo); + $pool->setPackages(array($package)); $this->assertEquals(array($package), $pool->whatProvides('foo')); $this->assertEquals(array($package), $pool->whatProvides('foo')); } - public function testPoolIgnoresIrrelevantPackages() - { - $pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE), array('bar' => BasePackage::STABILITY_BETA)); - $repo = new ArrayRepository; - $repo->addPackage($package = $this->getPackage('bar', '1')); - $repo->addPackage($betaPackage = $this->getPackage('bar', '1-beta')); - $repo->addPackage($alphaPackage = $this->getPackage('bar', '1-alpha')); - $repo->addPackage($package2 = $this->getPackage('foo', '1')); - $repo->addPackage($rcPackage2 = $this->getPackage('foo', '1rc')); - - $pool->addRepository($repo); - - $this->assertEquals(array($package, $betaPackage), $pool->whatProvides('bar')); - $this->assertEquals(array($package2), $pool->whatProvides('foo')); - } - - /** - * @expectedException \RuntimeException - */ - public function testGetPriorityForNotRegisteredRepository() - { - $pool = $this->createPool(); - $repository = new ArrayRepository; - - $pool->getPriority($repository); - } - - public function testGetPriorityWhenRepositoryIsRegistered() - { - $pool = $this->createPool(); - $firstRepository = new ArrayRepository; - $pool->addRepository($firstRepository); - $secondRepository = new ArrayRepository; - $pool->addRepository($secondRepository); - - $firstPriority = $pool->getPriority($firstRepository); - $secondPriority = $pool->getPriority($secondRepository); - - $this->assertEquals(0, $firstPriority); - $this->assertEquals(-1, $secondPriority); - } - - public function testWhatProvidesSamePackageForDifferentRepositories() - { - $pool = $this->createPool(); - $firstRepository = new ArrayRepository; - $secondRepository = new ArrayRepository; - - $firstPackage = $this->getPackage('foo', '1'); - $secondPackage = $this->getPackage('foo', '1'); - $thirdPackage = $this->getPackage('foo', '2'); - - $firstRepository->addPackage($firstPackage); - $secondRepository->addPackage($secondPackage); - $secondRepository->addPackage($thirdPackage); - - $pool->addRepository($firstRepository); - $pool->addRepository($secondRepository); - - $this->assertEquals(array($firstPackage, $secondPackage, $thirdPackage), $pool->whatProvides('foo')); - } - public function testWhatProvidesPackageWithConstraint() { $pool = $this->createPool(); - $repository = new ArrayRepository; $firstPackage = $this->getPackage('foo', '1'); $secondPackage = $this->getPackage('foo', '2'); - $repository->addPackage($firstPackage); - $repository->addPackage($secondPackage); - - $pool->addRepository($repository); + $pool->setPackages(array( + $firstPackage, + $secondPackage, + )); $this->assertEquals(array($firstPackage, $secondPackage), $pool->whatProvides('foo')); $this->assertEquals(array($secondPackage), $pool->whatProvides('foo', $this->getVersionConstraint('==', '2'))); @@ -114,11 +49,9 @@ class PoolTest extends TestCase public function testPackageById() { $pool = $this->createPool(); - $repository = new ArrayRepository; $package = $this->getPackage('foo', '1'); - $repository->addPackage($package); - $pool->addRepository($repository); + $pool->setPackages(array($package)); $this->assertSame($package, $pool->packageById(1)); } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index e9753c848..9acd45455 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -22,13 +22,6 @@ use Composer\TestCase; class RuleSetTest extends TestCase { - protected $pool; - - public function setUp() - { - $this->pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); - } - public function testAdd() { $rules = array( @@ -146,9 +139,11 @@ class RuleSetTest extends TestCase public function testPrettyString() { - $repo = new ArrayRepository; - $repo->addPackage($p = $this->getPackage('foo', '2.1')); - $this->pool->addRepository($repo); + $pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); + $pool->setPackages(array( + $p = $this->getPackage('foo', '2.1'), + )); + $p->setId(1); $ruleSet = new RuleSet; $literal = $p->getId(); @@ -156,7 +151,7 @@ class RuleSetTest extends TestCase $ruleSet->add($rule, RuleSet::TYPE_JOB); - $this->assertContains('JOB : Install command rule (install foo 2.1)', $ruleSet->getPrettyString($this->pool)); + $this->assertContains('JOB : Install command rule (install foo 2.1)', $ruleSet->getPrettyString($pool)); } private function getRuleMock() diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index c9b3dbe1a..cb83266c8 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -22,13 +22,6 @@ use Composer\TestCase; class RuleTest extends TestCase { - protected $pool; - - public function setUp() - { - $this->pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); - } - public function testGetHash() { $rule = new GenericRule(array(123), Rule::RULE_JOB_INSTALL, null); @@ -100,13 +93,16 @@ class RuleTest extends TestCase public function testPrettyString() { - $repo = new ArrayRepository; - $repo->addPackage($p1 = $this->getPackage('foo', '2.1')); - $repo->addPackage($p2 = $this->getPackage('baz', '1.1')); - $this->pool->addRepository($repo); + $pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); + $pool->setPackages(array( + $p1 = $this->getPackage('foo', '2.1'), + $p2 = $this->getPackage('baz', '1.1'), + )); + $p1->setId(1); + $p2->setId(2); $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_JOB_INSTALL, null); - $this->assertEquals('Install command rule (don\'t install baz 1.1|install foo 2.1)', $rule->getPrettyString($this->pool)); + $this->assertEquals('Install command rule (don\'t install baz 1.1|install foo 2.1)', $rule->getPrettyString($pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index e4b02968f..0d221a8ab 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -20,6 +20,7 @@ use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Solver; use Composer\DependencyResolver\SolverProblemsException; use Composer\Package\Link; +use Composer\Repository\InstalledArrayRepository; use Composer\Repository\RepositorySet; use Composer\TestCase; use Composer\Semver\Constraint\MultiConstraint; @@ -31,12 +32,13 @@ class SolverTest extends TestCase protected $repoInstalled; protected $request; protected $policy; + protected $solver; public function setUp() { $this->repoSet = new RepositorySet(array()); $this->repo = new ArrayRepository; - $this->repoInstalled = new ArrayRepository; + $this->repoInstalled = new InstalledArrayRepository; $this->request = new Request($this->repoSet); $this->policy = new DefaultPolicy; @@ -71,6 +73,7 @@ class SolverTest extends TestCase $this->request->install('B', $this->getVersionConstraint('==', '1')); + $this->createSolver(); try { $transaction = $this->solver->solve($this->request); $this->fail('Unsolvable conflict did not result in exception.'); @@ -94,8 +97,6 @@ class SolverTest extends TestCase $this->repoSet->addRepository($repo1); $this->repoSet->addRepository($repo2); - $this->solver = new Solver($this->policy, $this->repoSet->createPool(), $this->repoInstalled, new NullIO()); - $this->request->install('foo'); $this->checkSolverResult(array( @@ -447,6 +448,7 @@ class SolverTest extends TestCase // must explicitly pick the provider, so error in this case $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->createSolver(); $this->solver->solve($this->request); } @@ -480,6 +482,7 @@ class SolverTest extends TestCase $this->request->install('A'); $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->createSolver(); $this->solver->solve($this->request); } @@ -652,6 +655,7 @@ class SolverTest extends TestCase $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->createSolver(); $this->solver->solve($this->request); } @@ -668,6 +672,7 @@ class SolverTest extends TestCase $this->request->install('A'); $this->request->install('B'); + $this->createSolver(); try { $transaction = $this->solver->solve($this->request); $this->fail('Unsolvable conflict did not result in exception.'); @@ -697,6 +702,7 @@ class SolverTest extends TestCase $this->request->install('A'); + $this->createSolver(); try { $transaction = $this->solver->solve($this->request); $this->fail('Unsolvable conflict did not result in exception.'); @@ -744,6 +750,7 @@ class SolverTest extends TestCase $this->request->install('A'); + $this->createSolver(); try { $transaction = $this->solver->solve($this->request); $this->fail('Unsolvable conflict did not result in exception.'); @@ -843,12 +850,16 @@ class SolverTest extends TestCase { $this->repoSet->addRepository($this->repoInstalled); $this->repoSet->addRepository($this->repo); + } - $this->solver = new Solver($this->policy, $this->repoSet->createPool(), $this->repoInstalled, new NullIO()); + protected function createSolver() + { + $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), $this->repoInstalled, new NullIO()); } protected function checkSolverResult(array $expected) { + $this->createSolver(); $transaction = $this->solver->solve($this->request); $result = array(); diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 8614495ee..562a71518 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -30,7 +30,6 @@ use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Formatter\OutputFormatter; use Composer\TestCase; -use Composer\IO\BufferIO; class InstallerTest extends TestCase { From 96c812fb2479bdbb688ae750c637d7a66ad11b22 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 12 Sep 2018 13:27:10 +0200 Subject: [PATCH 010/321] Properly buffer installer test output to display as errors if necessary --- tests/Composer/Test/InstallerTest.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 562a71518..a244589af 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -14,6 +14,7 @@ namespace Composer\Test; use Composer\Installer; use Composer\Console\Application; +use Composer\IO\BufferIO; use Composer\Json\JsonFile; use Composer\Util\Filesystem; use Composer\Repository\ArrayRepository; @@ -56,7 +57,7 @@ class InstallerTest extends TestCase */ public function testInstaller(RootPackageInterface $rootPackage, $repositories, array $options) { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = new BufferIO('', OutputInterface::VERBOSITY_NORMAL, new OutputFormatter(false)); $downloadManager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($io)) @@ -81,7 +82,9 @@ class InstallerTest extends TestCase $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator); $result = $installer->run(); - $this->assertSame(0, $result); + + $output = str_replace("\r", '', $io->getOutput()); + $this->assertEquals(0, $result, $output); $expectedInstalled = isset($options['install']) ? $options['install'] : array(); $expectedUpdated = isset($options['update']) ? $options['update'] : array(); From 019ebee185a72745d284473dc9d27e50a186c69b Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 12 Sep 2018 13:56:13 +0200 Subject: [PATCH 011/321] Add missing use statement to package event to fix install --no-dev --- src/Composer/Installer/PackageEvent.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Installer/PackageEvent.php b/src/Composer/Installer/PackageEvent.php index a563a91ba..d2f427a92 100644 --- a/src/Composer/Installer/PackageEvent.php +++ b/src/Composer/Installer/PackageEvent.php @@ -18,6 +18,7 @@ use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\PolicyInterface; use Composer\DependencyResolver\Request; use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositorySet; /** * The Package Event. From 261efe1e8e957151b910d433721e0cecd818354e Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 12 Sep 2018 14:11:26 +0200 Subject: [PATCH 012/321] Implement loadPackages on Composer repositories with providers --- .../DependencyResolver/PoolBuilder.php | 5 ++++- src/Composer/Repository/BaseRepository.php | 4 ++-- .../Repository/ComposerRepository.php | 20 +++++++++++++++++++ .../Repository/RepositoryInterface.php | 3 ++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 2ec6dd680..32e14e983 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -16,6 +16,7 @@ use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Repository\AsyncRepositoryInterface; +use Composer\Repository\ComposerRepository; use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\LockArrayRepository; use Composer\Repository\PlatformRepository; @@ -83,9 +84,11 @@ class PoolBuilder } if ($repository instanceof AsyncRepositoryInterface) { + // TODO ispackageacceptablecallable in here? $packages = $repository->returnPackages($loadIds[$key]); } else { - $packages = $repository->loadPackages($loadNames); + // TODO should we really pass the callable into here? + $packages = $repository->loadPackages($loadNames, $this->isPackageAcceptableCallable); } foreach ($packages as $package) { diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/BaseRepository.php index e9e97bb91..172fe16b8 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/BaseRepository.php @@ -25,13 +25,13 @@ use Composer\Package\Link; abstract class BaseRepository implements RepositoryInterface { // TODO should this stay here? some repos need a better implementation - public function loadPackages(array $packageNameMap) + public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable) { $packages = $this->getPackages(); $result = array(); foreach ($packages as $package) { - if (isset($packageNameMap[$package->getName()])) { + if (isset($packageNameMap[$package->getName()]) && call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { $result[] = $package; } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index ea583ede6..739027628 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -194,6 +194,26 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return parent::getPackages(); } + public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable) + { + if (!$this->hasProviders()) { + // TODO build more efficient version of this + return parent::loadPackages($packageNameMap, $isPackageAcceptableCallable); + } + + $packages = array(); + foreach ($packageNameMap as $name => $void) { + $matches = array(); + foreach ($this->whatProvides($name, false, $isPackageAcceptableCallable) as $match) { + if ($match->getName() === $name) { + $matches[] = $match; + } + } + $packages = array_merge($packages, $matches); + } + return $packages; + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 703f92e35..55b76d33d 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -67,9 +67,10 @@ interface RepositoryInterface extends \Countable * Returns list of registered packages with the supplied name * * @param bool[] $packageNameMap + * @param $isPackageAcceptableCallable * @return PackageInterface[] */ - public function loadPackages(array $packageNameMap); + public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable); /** * Searches the repository for packages containing the query From 81bb8f81ad0cc37602dde4a1edae841ca1ab0935 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 12 Sep 2018 14:30:18 +0200 Subject: [PATCH 013/321] Set all package ids only once the pool is created They all get set in one place only and at a specific time when nothing else will possibly change them anymore --- src/Composer/DependencyResolver/Pool.php | 3 +++ src/Composer/DependencyResolver/PoolBuilder.php | 13 +------------ src/Composer/Repository/ComposerRepository.php | 10 ---------- .../Test/DependencyResolver/RuleSetTest.php | 1 - tests/Composer/Test/DependencyResolver/RuleTest.php | 2 -- 5 files changed, 4 insertions(+), 25 deletions(-) diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 6ad4d9f31..b5aefc4f7 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -68,7 +68,10 @@ class Pool implements \Countable $this->priorities = $priorities; $this->packages = $packages; + $id = 1; + foreach ($this->packages as $package) { + $package->id = $id++; $names = $package->getNames(); $this->packageByExactName[$package->getName()][$package->id] = $package; diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 32e14e983..e51fce4eb 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -32,7 +32,6 @@ class PoolBuilder private $loadedNames = array(); - private $id = 1; private $packages = array(); private $priorities = array(); @@ -57,14 +56,6 @@ class PoolBuilder } } - foreach ($repositories as $repository) { - if ($repository instanceof ComposerRepository && $repository->hasProviders()) { - $this->providerRepos[] = $repository; - $repository->setRootAliases($this->rootAliases); - $repository->resetPackageIds(); - } - } - while (!empty($loadNames)) { $loadIds = array(); foreach ($repositories as $key => $repository) { @@ -117,9 +108,8 @@ class PoolBuilder private function loadPackage(PackageInterface $package, $repoIndex) { - $package->setId($this->id++); $this->packages[] = $package; - $this->priorities[$this->id - 2] = -$repoIndex; + $this->priorities[] = -$repoIndex; // handle root package aliases $name = $package->getName(); @@ -130,7 +120,6 @@ class PoolBuilder } $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); $aliasPackage->setRootPackageAlias(true); - $aliasPackage->setId($this->id++); $package->getRepository()->addPackage($aliasPackage); // TODO do we need this? $this->packages[] = $aliasPackage; diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 739027628..c26ab8a62 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -297,16 +297,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $this->hasProviders; } - public function resetPackageIds() - { - foreach ($this->providersByUid as $package) { - if ($package instanceof AliasPackage) { - $package->getAliasOf()->setId(-1); - } - $package->setId(-1); - } - } - /** * @param string $name package name * @param bool $bypassFilters If set to true, this bypasses the stability filtering, and forces a recompute without cache diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 9acd45455..81712070b 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -143,7 +143,6 @@ class RuleSetTest extends TestCase $pool->setPackages(array( $p = $this->getPackage('foo', '2.1'), )); - $p->setId(1); $ruleSet = new RuleSet; $literal = $p->getId(); diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index cb83266c8..bc081f04a 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -98,8 +98,6 @@ class RuleTest extends TestCase $p1 = $this->getPackage('foo', '2.1'), $p2 = $this->getPackage('baz', '1.1'), )); - $p1->setId(1); - $p2->setId(2); $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_JOB_INSTALL, null); From 902cb290e792fdc3a4ff6a7af23ce7fea54b7acc Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 12 Sep 2018 16:32:14 +0200 Subject: [PATCH 014/321] Only load package versions which fit the root composer.json constraints --- .../DependencyResolver/PoolBuilder.php | 4 ++-- src/Composer/Repository/BaseRepository.php | 20 ++++++++++++++++--- .../Repository/ComposerRepository.php | 19 ++++++++++++++---- .../Fixtures/installer/solver-problems.test | 8 +++++--- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index e51fce4eb..2fd3c8230 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -51,7 +51,7 @@ class PoolBuilder foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': - $loadNames[$job['packageName']] = true; + $loadNames[$job['packageName']] = $job['constraint']; break; } } @@ -129,7 +129,7 @@ class PoolBuilder foreach ($package->getRequires() as $link) { $require = $link->getTarget(); if (!isset($this->loadedNames[$require])) { - $loadNames[$require] = true; + $loadNames[$require] = null; } } diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/BaseRepository.php index 172fe16b8..d835d55fd 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/BaseRepository.php @@ -12,6 +12,7 @@ namespace Composer\Repository; +use Composer\Package\AliasPackage; use Composer\Package\RootPackageInterface; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; @@ -25,14 +26,27 @@ use Composer\Package\Link; abstract class BaseRepository implements RepositoryInterface { // TODO should this stay here? some repos need a better implementation - public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable) + public function loadPackages(array $packageMap, $isPackageAcceptableCallable) { $packages = $this->getPackages(); $result = array(); foreach ($packages as $package) { - if (isset($packageNameMap[$package->getName()]) && call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { - $result[] = $package; + if (array_key_exists($package->getName(), $packageMap) && + (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) && + call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { + $result[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { + $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + } + } + } + + foreach ($packages as $package) { + if ($package instanceof AliasPackage) { + if (isset($result[spl_object_hash($package->getAliasOf())])) { + $result[spl_object_hash($package)] = $package; + } } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index c26ab8a62..09e2179d8 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -202,11 +202,22 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $packages = array(); - foreach ($packageNameMap as $name => $void) { + foreach ($packageNameMap as $name => $constraint) { $matches = array(); - foreach ($this->whatProvides($name, false, $isPackageAcceptableCallable) as $match) { - if ($match->getName() === $name) { - $matches[] = $match; + $candidates = $this->whatProvides($name, false, $isPackageAcceptableCallable); + foreach ($candidates as $candidate) { + if ($candidate->getName() === $name && (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion())))) { + $matches[spl_object_hash($candidate)] = $candidate; + if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) { + $matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf(); + } + } + } + foreach ($candidates as $candidate) { + if ($candidate instanceof AliasPackage) { + if (isset($result[spl_object_hash($candidate->getAliasOf())])) { + $matches[spl_object_hash($candidate)] = $candidate; + } } } $packages = array_merge($packages, $matches); diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index e0359a151..ce3c57ebb 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -42,14 +42,16 @@ Updating dependencies (including require-dev) Your requirements could not be resolved to an installable set of packages. Problem 1 - - The requested package unstable/package 2.* exists as unstable/package[1.0.0] but these are rejected by your constraint. + - The requested package unstable/package could not be found in any version, there may be a typo in the package name. Problem 2 - The requested package bogus could not be found in any version, there may be a typo in the package name. Problem 3 - - The requested package stable-requiree-excluded (installed at 1.0.0, required as 1.0.1) is satisfiable by stable-requiree-excluded[1.0.0] but these conflict with your requirements or minimum-stability. + - The requested package stable-requiree-excluded 1.0.1 exists as stable-requiree-excluded[1.0.0] but these are rejected by your constraint. Problem 4 + - The requested package stable-requiree-excluded (installed at 1.0.0, required as 1.0.1) is satisfiable by stable-requiree-excluded[1.0.0] but these conflict with your requirements or minimum-stability. + Problem 5 - Installation request for requirer 1.* -> satisfiable by requirer[1.0.0]. - - requirer 1.0.0 requires dependency 1.0.0 -> satisfiable by dependency[1.0.0] but these conflict with your requirements or minimum-stability. + - requirer 1.0.0 requires dependency 1.0.0 -> no matching package found. Potential causes: - A typo in the package name From 7c2d3518e5de241cefc403af8a9b31330b73ac6a Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 12 Sep 2018 19:03:57 +0200 Subject: [PATCH 015/321] Remove whitelisting of required package names, done by pool builder now --- src/Composer/DependencyResolver/Pool.php | 21 ------- .../DependencyResolver/RuleSetGenerator.php | 58 ------------------- 2 files changed, 79 deletions(-) diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index b5aefc4f7..35feafde8 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -49,7 +49,6 @@ class Pool implements \Countable protected $versionParser; protected $providerCache = array(); protected $filterRequires; - protected $whitelist = null; public function __construct(array $filterRequires = array()) { @@ -57,12 +56,6 @@ class Pool implements \Countable $this->versionParser = new VersionParser; } - public function setWhitelist($whitelist) - { - $this->whitelist = $whitelist; - $this->providerCache = array(); - } - public function setPackages(array $packages, array $priorities = array()) { $this->priorities = $priorities; @@ -149,20 +142,6 @@ class Pool implements \Countable $nameMatch = false; foreach ($candidates as $candidate) { - $aliasOfCandidate = null; - - // alias packages are not white listed, make sure that the package - // being aliased is white listed - if ($candidate instanceof AliasPackage) { - $aliasOfCandidate = $candidate->getAliasOf(); - } - - if ($this->whitelist !== null && !$bypassFilters && ( - (!($candidate instanceof AliasPackage) && !isset($this->whitelist[$candidate->id])) || - ($candidate instanceof AliasPackage && !isset($this->whitelist[$aliasOfCandidate->id])) - )) { - continue; - } switch ($this->match($candidate, $name, $constraint, $bypassFilters)) { case self::MATCH_NONE: break; diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 60617ba43..54811e38b 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -26,7 +26,6 @@ class RuleSetGenerator protected $rules; protected $jobs; protected $installedMap; - protected $whitelistedMap; protected $addedMap; protected $conflictAddedMap; protected $addedPackages; @@ -147,41 +146,6 @@ class RuleSetGenerator $this->rules->add($newRule, $type); } - protected function whitelistFromPackage(PackageInterface $package) - { - $workQueue = new \SplQueue; - $workQueue->enqueue($package); - - while (!$workQueue->isEmpty()) { - $package = $workQueue->dequeue(); - if (isset($this->whitelistedMap[$package->id])) { - continue; - } - - $this->whitelistedMap[$package->id] = true; - - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true); - - foreach ($possibleRequires as $require) { - $workQueue->enqueue($require); - } - } - - $obsoleteProviders = $this->pool->whatProvides($package->getName(), null, true); - - foreach ($obsoleteProviders as $provider) { - if ($provider === $package) { - continue; - } - - if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { - $workQueue->enqueue($provider); - } - } - } - } - protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs) { $workQueue = new \SplQueue; @@ -290,20 +254,6 @@ class RuleSetGenerator return $impossible; } - protected function whitelistFromJobs() - { - foreach ($this->jobs as $job) { - switch ($job['cmd']) { - case 'install': - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true); - foreach ($packages as $package) { - $this->whitelistFromPackage($package); - } - break; - } - } - } - protected function addRulesForJobs($ignorePlatformReqs) { foreach ($this->jobs as $job) { @@ -344,14 +294,6 @@ class RuleSetGenerator $this->rules = new RuleSet; $this->installedMap = $installedMap; - $this->whitelistedMap = array(); - foreach ($this->installedMap as $package) { - $this->whitelistFromPackage($package); - } - $this->whitelistFromJobs(); - - $this->pool->setWhitelist($this->whitelistedMap); - $this->addedMap = array(); $this->conflictAddedMap = array(); $this->addedPackages = array(); From b757c1952c2fdca5a4a745a87e03cbd38af69303 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 13 Sep 2018 15:23:51 +0200 Subject: [PATCH 016/321] Fix phpdoc --- src/Composer/Repository/AsyncRepositoryInterface.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/AsyncRepositoryInterface.php b/src/Composer/Repository/AsyncRepositoryInterface.php index 91f543dbf..5804694ec 100644 --- a/src/Composer/Repository/AsyncRepositoryInterface.php +++ b/src/Composer/Repository/AsyncRepositoryInterface.php @@ -25,13 +25,13 @@ interface AsyncRepositoryInterface { /** * @param array $names Names of packages to retrieve data for - * @return scalar Id to be passed to later loadPackages call + * @return mixed Id to be passed to later loadPackages call */ public function requestPackages(array $names); /** * @param array $names - * @return scalar id for load call + * @return mixed id for load call */ public function returnPackages($loadId); } From f11c3573255c25f3798e4150fbbf649139472f2f Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 14 Sep 2018 14:39:24 +0200 Subject: [PATCH 017/321] Restore output of number of packages analyzed in solver --- src/Composer/Installer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 95d5b6605..cb4cc8c5d 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -491,6 +491,7 @@ class Installer $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations); + $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE); // execute operations From 83efeaec5cda2f2fd75e7521cfd66bd511c4d019 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 14 Sep 2018 14:40:34 +0200 Subject: [PATCH 018/321] Attempt to prune versions which are impossible to install during pool building --- src/Composer/DependencyResolver/Pool.php | 8 ++-- .../DependencyResolver/PoolBuilder.php | 39 ++++++++++++++++++- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 35feafde8..355ba25d4 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -58,12 +58,12 @@ class Pool implements \Countable public function setPackages(array $packages, array $priorities = array()) { - $this->priorities = $priorities; - $this->packages = $packages; - $id = 1; - foreach ($this->packages as $package) { + foreach ($packages as $i => $package) { + $this->packages[] = $package; + $this->priorities[] = isset($priorities[$i]) ? $priorities[$i] : 0; + $package->id = $id++; $names = $package->getNames(); $this->packageByExactName[$package->getName()][$package->id] = $package; diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 2fd3c8230..9c5e48efa 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -20,6 +20,8 @@ use Composer\Repository\ComposerRepository; use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\LockArrayRepository; use Composer\Repository\PlatformRepository; +use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\MultiConstraint; /** * @author Nils Adermann @@ -30,6 +32,9 @@ class PoolBuilder private $filterRequires; private $rootAliases; + private $aliasMap = array(); + private $nameConstraints = array(); + private $loadedNames = array(); private $packages = array(); @@ -52,6 +57,7 @@ class PoolBuilder switch ($job['cmd']) { case 'install': $loadNames[$job['packageName']] = $job['constraint']; + $this->nameConstraints[$job['packageName']] = $job['constraint'] ? new MultiConstraint(array($job['constraint']), false) : null; break; } } @@ -92,6 +98,17 @@ class PoolBuilder $loadNames = $newLoadNames; } + foreach ($this->packages as $i => $package) { + if (!$package instanceof AliasPackage && !isset($this->aliasMap[spl_object_hash($package)]) && isset($this->nameConstraints[$package->getName()])) { + $constraint = $this->nameConstraints[$package->getName()]; + + if ($constraint && !$constraint->matches(new Constraint('==', $package->getVersion()))) { + unset($this->packages[$i]); + unset($this->priorities[$i]); + } + } + } + foreach ($repositories as $key => $repository) { if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface) { @@ -111,18 +128,26 @@ class PoolBuilder $this->packages[] = $package; $this->priorities[] = -$repoIndex; + if ($package instanceof AliasPackage) { + $this->aliasMap[spl_object_hash($package->getAliasOf())][] = $package; + } + // handle root package aliases $name = $package->getName(); if (isset($this->rootAliases[$name][$package->getVersion()])) { $alias = $this->rootAliases[$name][$package->getVersion()]; if ($package instanceof AliasPackage) { - $package = $package->getAliasOf(); + $basePackage = $package->getAliasOf(); + } else { + $basePackage = $package; } - $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); + $aliasPackage = new AliasPackage($basePackage, $alias['alias_normalized'], $alias['alias']); $aliasPackage->setRootPackageAlias(true); $package->getRepository()->addPackage($aliasPackage); // TODO do we need this? $this->packages[] = $aliasPackage; + $this->priorities[] = -$repoIndex; + $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][] = $aliasPackage; } $loadNames = array(); @@ -131,6 +156,16 @@ class PoolBuilder if (!isset($this->loadedNames[$require])) { $loadNames[$require] = null; } + if ($link->getConstraint()) { + if (!array_key_exists($require, $this->nameConstraints)) { + $this->nameConstraints[$require] = new MultiConstraint(array($link->getConstraint()), false); + } elseif ($this->nameConstraints[$require]) { + // TODO addConstraint function? + $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($link->getConstraint()), $this->nameConstraints[$require]->getConstraints()), false); + } + } else { + $this->nameConstraints[$require] = null; + } } return $loadNames; From 537f4fbc3b487029b45c277744a34bc30890d2aa Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 14 Sep 2018 15:03:38 +0200 Subject: [PATCH 019/321] Prune unreachable required versions correctly for aliased packages In trials this seems pointless, so maybe better to skip aliases and reduce memory and cpu wasted on looking these things up --- .../DependencyResolver/PoolBuilder.php | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 9c5e48efa..8ab6e5244 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -99,12 +99,27 @@ class PoolBuilder } foreach ($this->packages as $i => $package) { - if (!$package instanceof AliasPackage && !isset($this->aliasMap[spl_object_hash($package)]) && isset($this->nameConstraints[$package->getName()])) { + // we check all alias related packages at once, so no need ot check individual aliases + // isset also checks non-null value + if (!$package instanceof AliasPackage && isset($this->nameConstraints[$package->getName()])) { $constraint = $this->nameConstraints[$package->getName()]; - if ($constraint && !$constraint->matches(new Constraint('==', $package->getVersion()))) { - unset($this->packages[$i]); - unset($this->priorities[$i]); + $aliasedPackages = array($i => $package); + if (isset($this->aliasMap[spl_object_hash($package)])) { + $aliasedPackages += $this->aliasMap[spl_object_hash($package)]; + } + + $found = false; + foreach ($aliasedPackages as $packageOrAlias) { + if ($constraint->matches(new Constraint('==', $packageOrAlias->getVersion()))) { + $found = true; + } + } + if (!$found) { + foreach ($aliasedPackages as $index => $packageOrAlias) { + unset($this->packages[$index]); + unset($this->priorities[$index]); + } } } } @@ -120,16 +135,21 @@ class PoolBuilder $this->pool->setPackages($this->packages, $this->priorities); + unset($this->aliasMap); + unset($this->loadedNames); + unset($this->nameConstraints); + return $this->pool; } private function loadPackage(PackageInterface $package, $repoIndex) { + $index = count($this->packages); $this->packages[] = $package; $this->priorities[] = -$repoIndex; if ($package instanceof AliasPackage) { - $this->aliasMap[spl_object_hash($package->getAliasOf())][] = $package; + $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package; } // handle root package aliases @@ -147,7 +167,7 @@ class PoolBuilder $package->getRepository()->addPackage($aliasPackage); // TODO do we need this? $this->packages[] = $aliasPackage; $this->priorities[] = -$repoIndex; - $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][] = $aliasPackage; + $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$index+1] = $aliasPackage; } $loadNames = array(); From 5a56bb69710cbc2d8fd8fc83fc0654de11059e7f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 12 Nov 2018 15:57:44 +0100 Subject: [PATCH 020/321] Remove BC event and constraint classes --- .../EventDispatcher/EventDispatcher.php | 38 ------------------ .../LinkConstraint/EmptyConstraint.php | 24 ------------ .../LinkConstraintInterface.php | 24 ------------ .../LinkConstraint/MultiConstraint.php | 24 ------------ .../LinkConstraint/SpecificConstraint.php | 24 ------------ .../LinkConstraint/VersionConstraint.php | 24 ------------ src/Composer/Script/CommandEvent.php | 22 ----------- src/Composer/Script/PackageEvent.php | 24 ------------ src/Composer/Util/SpdxLicense.php | 24 ------------ src/Composer/XdebugHandler.php | 31 --------------- .../EventDispatcher/EventDispatcherTest.php | 39 ++----------------- 11 files changed, 3 insertions(+), 295 deletions(-) delete mode 100644 src/Composer/Package/LinkConstraint/EmptyConstraint.php delete mode 100644 src/Composer/Package/LinkConstraint/LinkConstraintInterface.php delete mode 100644 src/Composer/Package/LinkConstraint/MultiConstraint.php delete mode 100644 src/Composer/Package/LinkConstraint/SpecificConstraint.php delete mode 100644 src/Composer/Package/LinkConstraint/VersionConstraint.php delete mode 100644 src/Composer/Script/CommandEvent.php delete mode 100644 src/Composer/Script/PackageEvent.php delete mode 100644 src/Composer/Util/SpdxLicense.php delete mode 100644 src/Composer/XdebugHandler.php diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index f0fcdaef6..324190f19 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -321,44 +321,6 @@ class EventDispatcher $expected = $typehint->getName(); - // BC support - if (!$event instanceof $expected && $expected === 'Composer\Script\CommandEvent') { - trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED); - $event = new \Composer\Script\CommandEvent( - $event->getName(), - $event->getComposer(), - $event->getIO(), - $event->isDevMode(), - $event->getArguments() - ); - } - if (!$event instanceof $expected && $expected === 'Composer\Script\PackageEvent') { - trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED); - $event = new \Composer\Script\PackageEvent( - $event->getName(), - $event->getComposer(), - $event->getIO(), - $event->isDevMode(), - $event->getPolicy(), - $event->getRepositorySet(), - $event->getInstalledRepo(), - $event->getRequest(), - $event->getOperations(), - $event->getOperation() - ); - } - if (!$event instanceof $expected && $expected === 'Composer\Script\Event') { - trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED); - $event = new \Composer\Script\Event( - $event->getName(), - $event->getComposer(), - $event->getIO(), - $event->isDevMode(), - $event->getArguments(), - $event->getFlags() - ); - } - return $event; } diff --git a/src/Composer/Package/LinkConstraint/EmptyConstraint.php b/src/Composer/Package/LinkConstraint/EmptyConstraint.php deleted file mode 100644 index 33f9e2e82..000000000 --- a/src/Composer/Package/LinkConstraint/EmptyConstraint.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\EmptyConstraint as SemverEmptyConstraint; - -trigger_error('The ' . __NAMESPACE__ . '\EmptyConstraint class is deprecated, use Composer\Semver\Constraint\EmptyConstraint instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\EmptyConstraint instead - */ -class EmptyConstraint extends SemverEmptyConstraint implements LinkConstraintInterface -{ -} diff --git a/src/Composer/Package/LinkConstraint/LinkConstraintInterface.php b/src/Composer/Package/LinkConstraint/LinkConstraintInterface.php deleted file mode 100644 index b8903ea8f..000000000 --- a/src/Composer/Package/LinkConstraint/LinkConstraintInterface.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\ConstraintInterface; - -trigger_error('The ' . __NAMESPACE__ . '\LinkConstraintInterface interface is deprecated, use Composer\Semver\Constraint\ConstraintInterface instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\ConstraintInterface instead - */ -interface LinkConstraintInterface extends ConstraintInterface -{ -} diff --git a/src/Composer/Package/LinkConstraint/MultiConstraint.php b/src/Composer/Package/LinkConstraint/MultiConstraint.php deleted file mode 100644 index 10a996568..000000000 --- a/src/Composer/Package/LinkConstraint/MultiConstraint.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\MultiConstraint as SemverMultiConstraint; - -trigger_error('The ' . __NAMESPACE__ . '\MultiConstraint class is deprecated, use Composer\Semver\Constraint\MultiConstraint instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\MultiConstraint instead - */ -class MultiConstraint extends SemverMultiConstraint implements LinkConstraintInterface -{ -} diff --git a/src/Composer/Package/LinkConstraint/SpecificConstraint.php b/src/Composer/Package/LinkConstraint/SpecificConstraint.php deleted file mode 100644 index 12d3194b6..000000000 --- a/src/Composer/Package/LinkConstraint/SpecificConstraint.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\AbstractConstraint; - -trigger_error('The ' . __NAMESPACE__ . '\SpecificConstraint abstract class is deprecated, there is no replacement for it.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\AbstractConstraint instead - */ -abstract class SpecificConstraint extends AbstractConstraint implements LinkConstraintInterface -{ -} diff --git a/src/Composer/Package/LinkConstraint/VersionConstraint.php b/src/Composer/Package/LinkConstraint/VersionConstraint.php deleted file mode 100644 index d6b1cbe1d..000000000 --- a/src/Composer/Package/LinkConstraint/VersionConstraint.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Package\LinkConstraint; - -use Composer\Semver\Constraint\Constraint; - -trigger_error('The ' . __NAMESPACE__ . '\VersionConstraint class is deprecated, use Composer\Semver\Constraint\Constraint instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Semver\Constraint\Constraint instead - */ -class VersionConstraint extends Constraint implements LinkConstraintInterface -{ -} diff --git a/src/Composer/Script/CommandEvent.php b/src/Composer/Script/CommandEvent.php deleted file mode 100644 index 84c52008c..000000000 --- a/src/Composer/Script/CommandEvent.php +++ /dev/null @@ -1,22 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Script; - -/** - * The Command Event. - * - * @deprecated use Composer\Script\Event instead - */ -class CommandEvent extends Event -{ -} diff --git a/src/Composer/Script/PackageEvent.php b/src/Composer/Script/PackageEvent.php deleted file mode 100644 index 531b86a40..000000000 --- a/src/Composer/Script/PackageEvent.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Script; - -use Composer\Installer\PackageEvent as BasePackageEvent; - -/** - * The Package Event. - * - * @deprecated Use Composer\Installer\PackageEvent instead - */ -class PackageEvent extends BasePackageEvent -{ -} diff --git a/src/Composer/Util/SpdxLicense.php b/src/Composer/Util/SpdxLicense.php deleted file mode 100644 index be4efdc54..000000000 --- a/src/Composer/Util/SpdxLicense.php +++ /dev/null @@ -1,24 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Util; - -use Composer\Spdx\SpdxLicenses; - -trigger_error('The ' . __NAMESPACE__ . '\SpdxLicense class is deprecated, use Composer\Spdx\SpdxLicenses instead.', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\Spdx\SpdxLicenses instead - */ -class SpdxLicense extends SpdxLicenses -{ -} diff --git a/src/Composer/XdebugHandler.php b/src/Composer/XdebugHandler.php deleted file mode 100644 index eb94e93f4..000000000 --- a/src/Composer/XdebugHandler.php +++ /dev/null @@ -1,31 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer; - -use Symfony\Component\Console\Output\OutputInterface; - -trigger_error('The ' . __NAMESPACE__ . '\XdebugHandler class is deprecated, use Composer\XdebugHandler\XdebugHandler instead,', E_USER_DEPRECATED); - -/** - * @deprecated use Composer\XdebugHandler\XdebugHandler instead - */ -class XdebugHandler extends XdebugHandler\XdebugHandler -{ - const ENV_ALLOW = 'COMPOSER_ALLOW_XDEBUG'; - const ENV_VERSION = 'COMPOSER_XDEBUG_VERSION'; - - public function __construct(OutputInterface $output) - { - parent::__construct('composer', '--ansi'); - } -} diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 54460d705..590711d79 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -20,7 +20,7 @@ use Composer\Composer; use Composer\TestCase; use Composer\IO\BufferIO; use Composer\Script\ScriptEvents; -use Composer\Script\CommandEvent; +use Composer\Script\Event as ScriptEvent; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Output\OutputInterface; @@ -51,29 +51,6 @@ class EventDispatcherTest extends TestCase $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false); } - /** - * @expectedException PHPUnit_Framework_Error_Deprecated - */ - public function testDispatcherCanConvertScriptEventToCommandEventForListener() - { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $dispatcher = $this->getDispatcherStubForListenersTest(array( - 'Composer\Test\EventDispatcher\EventDispatcherTest::expectsCommandEvent', - ), $io); - - $this->assertEquals(1, $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false)); - } - - public function testDispatcherDoesNotAttemptConversionForListenerWithoutTypehint() - { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $dispatcher = $this->getDispatcherStubForListenersTest(array( - 'Composer\Test\EventDispatcher\EventDispatcherTest::expectsVariableEvent', - ), $io); - - $this->assertEquals(1, $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false)); - } - /** * @dataProvider getValidCommands * @param string $command @@ -265,7 +242,7 @@ class EventDispatcherTest extends TestCase return array(); })); - $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io)); + $dispatcher->dispatch('root', new ScriptEvent('root', $composer, $io)); $expected = '> root: @group'.PHP_EOL. '> group: echo -n foo'.PHP_EOL. '> group: @subgroup'.PHP_EOL. @@ -305,7 +282,7 @@ class EventDispatcherTest extends TestCase return array(); })); - $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io)); + $dispatcher->dispatch('root', new ScriptEvent('root', $composer, $io)); } private function getDispatcherStubForListenersTest($listeners, $io) @@ -424,16 +401,6 @@ class EventDispatcherTest extends TestCase throw new \RuntimeException(); } - public static function expectsCommandEvent(CommandEvent $event) - { - return false; - } - - public static function expectsVariableEvent($event) - { - return false; - } - public static function someMethod() { return true; From bf33eec912fcdd2cdfc903b5656596ad0202c718 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 3 Dec 2018 10:59:04 +0100 Subject: [PATCH 021/321] Fix tests --- tests/Composer/Test/EventDispatcher/EventDispatcherTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index c3c20e7e8..7786e7807 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -284,7 +284,7 @@ class EventDispatcherTest extends TestCase return array(); })); - $dispatcher->dispatch('helloWorld', new CommandEvent('helloWorld', $composer, $io)); + $dispatcher->dispatch('helloWorld', new ScriptEvent('helloWorld', $composer, $io)); $expected = "> helloWorld: @hello World".PHP_EOL. "> hello: echo Hello " .escapeshellarg('World').PHP_EOL; From 554805197766b5f90d711e233706a92634f84536 Mon Sep 17 00:00:00 2001 From: Ahammar Yassine Date: Tue, 4 Dec 2018 16:03:16 +0100 Subject: [PATCH 022/321] Ask confirmation when is run as admin --- src/Composer/Console/Application.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index e6ff7da9d..9478561fb 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -207,6 +207,12 @@ class Application extends BaseApplication if (function_exists('posix_getuid') && posix_getuid() === 0) { if ($commandName !== 'self-update' && $commandName !== 'selfupdate') { $io->writeError('Do not run Composer as root/super user! See https://getcomposer.org/root for details'); + + if ($io->isInteractive()) { + if (!$io->askConfirmation('Continue as root/super user [yes]? ', true)) { + exit(0); + } + } } if ($uid = (int) getenv('SUDO_UID')) { // Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on From 0fd4ef6d8eaa29d887704d0fcc0eaf2b302dddbe Mon Sep 17 00:00:00 2001 From: Ahammar Yassine Date: Tue, 4 Dec 2018 17:47:45 +0100 Subject: [PATCH 023/321] Ask confirmation when is run as admin Use return instead of exit for the Application to run cleanly --- src/Composer/Console/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 9478561fb..c148c73fa 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -210,7 +210,7 @@ class Application extends BaseApplication if ($io->isInteractive()) { if (!$io->askConfirmation('Continue as root/super user [yes]? ', true)) { - exit(0); + return 0; } } } From b4fae00db24cc08ad5d5c3b5e2dec8c6156d5d39 Mon Sep 17 00:00:00 2001 From: Ahammar Yassine Date: Tue, 4 Dec 2018 18:54:57 +0100 Subject: [PATCH 024/321] Change return code to 1 --- src/Composer/Console/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index c148c73fa..3398cef69 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -210,7 +210,7 @@ class Application extends BaseApplication if ($io->isInteractive()) { if (!$io->askConfirmation('Continue as root/super user [yes]? ', true)) { - return 0; + return 1; } } } From 56805ecafe8597a07b4576b45e5532b9862fd65a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 12 Sep 2018 18:58:54 +0200 Subject: [PATCH 025/321] Add HttpDownloader to wrap/replace RemoteFilesystem with a new curl multi implementation --- composer.json | 3 +- composer.lock | 46 ++- src/Composer/Downloader/FileDownloader.php | 20 +- src/Composer/Downloader/GzipDownloader.php | 6 +- src/Composer/Downloader/RarDownloader.php | 6 +- src/Composer/Downloader/XzDownloader.php | 6 +- src/Composer/Downloader/ZipDownloader.php | 6 +- src/Composer/Factory.php | 14 +- src/Composer/Plugin/PreFileDownloadEvent.php | 20 +- .../Repository/ComposerRepository.php | 220 +++++++++++--- src/Composer/Repository/RepositoryFactory.php | 6 +- src/Composer/Repository/RepositoryManager.php | 6 +- src/Composer/Util/Http/CurlDownloader.php | 282 ++++++++++++++++++ src/Composer/Util/Http/Response.php | 75 +++++ src/Composer/Util/HttpDownloader.php | 246 +++++++++++++++ src/Composer/Util/RemoteFilesystem.php | 108 +------ src/Composer/Util/StreamContextFactory.php | 105 +++++++ 17 files changed, 985 insertions(+), 190 deletions(-) create mode 100644 src/Composer/Util/Http/CurlDownloader.php create mode 100644 src/Composer/Util/Http/Response.php create mode 100644 src/Composer/Util/HttpDownloader.php diff --git a/composer.json b/composer.json index 41048903b..1b75131bc 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "symfony/console": "^2.7 || ^3.0 || ^4.0", "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", "symfony/finder": "^2.7 || ^3.0 || ^4.0", - "symfony/process": "^2.7 || ^3.0 || ^4.0" + "symfony/process": "^2.7 || ^3.0 || ^4.0", + "react/promise": "^1.2" }, "conflict": { "symfony/console": "2.8.38" diff --git a/composer.lock b/composer.lock index 957382bc6..63b8033b9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e46280c4cfd37bf3ec8be36095feb20e", + "content-hash": "d356b92e869790db1e9d2c0f4b10935e", "packages": [ { "name": "composer/ca-bundle", @@ -342,6 +342,50 @@ ], "time": "2018-11-20T15:27:04+00:00" }, + { + "name": "react/promise", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "eefff597e67ff66b719f8171480add3c91474a1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/eefff597e67ff66b719f8171480add3c91474a1e", + "reference": "eefff597e67ff66b719f8171480add3c91474a1e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-0": { + "React\\Promise": "src/" + }, + "files": [ + "src/React/Promise/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "time": "2016-03-07T13:46:50+00:00" + }, { "name": "seld/jsonlint", "version": "1.7.1", diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 6596d9c8b..6b1349bb8 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -24,7 +24,7 @@ use Composer\Plugin\PluginEvents; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\Url as UrlUtil; /** @@ -39,7 +39,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface { protected $io; protected $config; - protected $rfs; + protected $httpDownloader; protected $filesystem; protected $cache; protected $outputProgress = true; @@ -52,16 +52,16 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface * @param IOInterface $io The IO instance * @param Config $config The config * @param EventDispatcher $eventDispatcher The event dispatcher - * @param Cache $cache Optional cache instance - * @param RemoteFilesystem $rfs The remote filesystem + * @param Cache $cache Cache instance + * @param HttpDownloader $httpDownloader The remote filesystem * @param Filesystem $filesystem The filesystem */ - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher, Cache $cache, HttpDownloader $httpDownloader, Filesystem $filesystem = null) { $this->io = $io; $this->config = $config; $this->eventDispatcher = $eventDispatcher; - $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader; $this->filesystem = $filesystem ?: new Filesystem(); $this->cache = $cache; @@ -125,13 +125,12 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $fileName = $this->getFileName($package, $path); $processedUrl = $this->processUrl($package, $url); - $hostname = parse_url($processedUrl, PHP_URL_HOST); - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl); + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $processedUrl); if ($this->eventDispatcher) { $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); } - $rfs = $preFileDownloadEvent->getRemoteFilesystem(); + $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); try { $checksum = $package->getDistSha1Checksum(); @@ -150,7 +149,8 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $retries = 3; while ($retries--) { try { - $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress, $package->getTransportOptions()); + // TODO handle this->outputProgress + $httpDownloader->copy($processedUrl, $fileName, $package->getTransportOptions()); break; } catch (TransportException $e) { // if we got an http response with a proper code, then requesting again will probably not help, abort diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index 19e4a45e1..bb86f6267 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -18,7 +18,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; /** @@ -30,10 +30,10 @@ class GzipDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, HttpDownloader $downloader = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); + parent::__construct($io, $config, $eventDispatcher, $cache, $downloader); } protected function extract($file, $path) diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index 40cd09896..c1ed8d60b 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -18,7 +18,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; use RarArchive; @@ -33,10 +33,10 @@ class RarDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, HttpDownloader $downloader = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); + parent::__construct($io, $config, $eventDispatcher, $cache, $downloader); } protected function extract($file, $path) diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 4a9b854d3..1f5947997 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -17,7 +17,7 @@ use Composer\Cache; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; /** @@ -30,11 +30,11 @@ class XzDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, HttpDownloader $downloader = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); + parent::__construct($io, $config, $eventDispatcher, $cache, $downloader); } protected function extract($file, $path) diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 6534db3d8..10518e026 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -19,7 +19,7 @@ use Composer\Package\PackageInterface; use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; use Symfony\Component\Process\ExecutableFinder; use ZipArchive; @@ -36,10 +36,10 @@ class ZipDownloader extends ArchiveDownloader protected $process; private $zipArchiveObject; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, HttpDownloader $downloader = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); + parent::__construct($io, $config, $eventDispatcher, $cache, $downloader); } /** diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 1aac934a1..97d507b10 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -23,7 +23,7 @@ use Composer\Repository\WritableRepositoryInterface; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\Silencer; use Composer\Plugin\PluginEvents; use Composer\EventDispatcher\Event; @@ -325,7 +325,7 @@ class Factory $io->loadConfiguration($config); } - $rfs = self::createRemoteFilesystem($io, $config); + $rfs = self::createHttpDownloader($io, $config); // initialize event dispatcher $dispatcher = new EventDispatcher($composer, $io); @@ -451,7 +451,7 @@ class Factory * @param EventDispatcher $eventDispatcher * @return Downloader\DownloadManager */ - public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, HttpDownloader $rfs = null) { $cache = null; if ($config->get('cache-files-ttl') > 0) { @@ -579,10 +579,10 @@ class Factory /** * @param IOInterface $io IO instance * @param Config $config Config instance - * @param array $options Array of options passed directly to RemoteFilesystem constructor - * @return RemoteFilesystem + * @param array $options Array of options passed directly to HttpDownloader constructor + * @return HttpDownloader */ - public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array()) + public static function createHttpDownloader(IOInterface $io, Config $config = null, $options = array()) { static $warned = false; $disableTls = false; @@ -607,7 +607,7 @@ class Factory $remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options); } try { - $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls); + $remoteFilesystem = new HttpDownloader($io, $config, $remoteFilesystemOptions, $disableTls); } catch (TransportException $e) { if (false !== strpos($e->getMessage(), 'cafile')) { $io->write('Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.'); diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 7ae6821ce..076449484 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -13,7 +13,7 @@ namespace Composer\Plugin; use Composer\EventDispatcher\Event; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; /** * The pre file download event. @@ -23,7 +23,7 @@ use Composer\Util\RemoteFilesystem; class PreFileDownloadEvent extends Event { /** - * @var RemoteFilesystem + * @var HttpDownloader */ private $rfs; @@ -36,10 +36,10 @@ class PreFileDownloadEvent extends Event * Constructor. * * @param string $name The event name - * @param RemoteFilesystem $rfs + * @param HttpDownloader $rfs * @param string $processedUrl */ - public function __construct($name, RemoteFilesystem $rfs, $processedUrl) + public function __construct($name, HttpDownloader $rfs, $processedUrl) { parent::__construct($name); $this->rfs = $rfs; @@ -47,21 +47,17 @@ class PreFileDownloadEvent extends Event } /** - * Returns the remote filesystem - * - * @return RemoteFilesystem + * @return HttpDownloader */ - public function getRemoteFilesystem() + public function getHttpDownloader() { return $this->rfs; } /** - * Sets the remote filesystem - * - * @param RemoteFilesystem $rfs + * @param HttpDownloader $rfs */ - public function setRemoteFilesystem(RemoteFilesystem $rfs) + public function setHttpDownloader(HttpDownloader $rfs) { $this->rfs = $rfs; } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 09e2179d8..c4a570c75 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -21,7 +21,7 @@ use Composer\Cache; use Composer\Config; use Composer\Factory; use Composer\IO\IOInterface; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Plugin\PluginEvents; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; @@ -40,7 +40,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito protected $url; protected $baseUrl; protected $io; - protected $rfs; + protected $httpDownloader; protected $cache; protected $notifyUrl; protected $searchUrl; @@ -60,8 +60,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $rootData; private $hasPartialPackages; private $partialPackagesByName; + private $versionParser; - public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher, HttpDownloader $httpDownloader) { parent::__construct(); if (!preg_match('{^[\w.]+\??://}', $repoConfig['url'])) { @@ -98,12 +99,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->baseUrl = rtrim(preg_replace('{(?:/[^/\\\\]+\.json)?(?:[?#].*)?$}', '', $this->url), '/'); $this->io = $io; $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); - $this->loader = new ArrayLoader(); - if ($rfs && $this->options) { - $rfs = clone $rfs; - $rfs->setOptions($this->options); + $this->versionParser = new VersionParser(); + $this->loader = new ArrayLoader($this->versionParser); + if ($httpDownloader && $this->options) { + // TODO solve this somehow - should be sent a request time not on the instance + $httpDownloader = clone $httpDownloader; + $httpDownloader->setOptions($this->options); } - $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $this->config, $this->options); + $this->httpDownloader = $httpDownloader; $this->eventDispatcher = $eventDispatcher; $this->repoConfig = $repoConfig; } @@ -129,8 +132,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $name = strtolower($name); if (!$constraint instanceof ConstraintInterface) { - $versionParser = new VersionParser(); - $constraint = $versionParser->parseConstraints($constraint); + $constraint = $this->versionParser->parseConstraints($constraint); } foreach ($this->getProviderNames() as $providerName) { @@ -161,8 +163,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $name = strtolower($name); if (null !== $constraint && !$constraint instanceof ConstraintInterface) { - $versionParser = new VersionParser(); - $constraint = $versionParser->parseConstraints($constraint); + $constraint = $this->versionParser->parseConstraints($constraint); } $packages = array(); @@ -196,8 +197,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable) { + if ($this->lazyProvidersUrl) { + return $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable); + } if (!$this->hasProviders()) { - // TODO build more efficient version of this return parent::loadPackages($packageNameMap, $isPackageAcceptableCallable); } @@ -235,9 +238,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) { $url = str_replace(array('%query%', '%type%'), array($query, $type), $this->searchUrl); - $hostname = parse_url($url, PHP_URL_HOST) ?: $url; - $json = $this->rfs->getContents($hostname, $url, false); - $search = JsonFile::parseJson($json, $url); + $search = $this->httpDownloader->get($url)->decodeJson(); if (empty($search['results'])) { return array(); @@ -496,6 +497,85 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->configurePackageTransportOptions($package); } + private function loadAsyncPackages(array $packageNames, $isPackageAcceptableCallable) + { + $this->loadRootServerFile(); + + $packages = array(); + $repo = $this; + + $createPackageIfAcceptable = function ($version, $constraint) use (&$packages, $isPackageAcceptableCallable, $repo) { + if (!call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { + return; + } + + if (isset($version['version_normalized']) && $constraint && !$constraint->matches(new Constraint('==', $version['version_normalized']))) { + return; + } + + // load acceptable packages in the providers + $package = $this->createPackage($version, 'Composer\Package\CompletePackage'); + $package->setRepository($repo); + + // if there was no version_normalized, then we need to check now for the constraint + if (!$constraint || isset($version['version_normalized']) || $constraint->matches(new Constraint('==', $package->getVersion()))) { + $packages[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { + $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + } + } + }; + + if ($this->lazyProvidersUrl) { + foreach ($packageNames as $name => $constraint) { + $url = str_replace('%package%', $name, $this->lazyProvidersUrl); + $cacheKey = 'provider-'.strtr($name, '/', '$').'.json'; + + $lastModified = null; + if ($contents = $this->cache->read($cacheKey)) { + $contents = json_decode($contents, true); + $lastModified = isset($contents['last-modified']) ? $contents['last-modified'] : null; + } + + $this->asyncFetchFile($url, $cacheKey, $lastModified) + ->then(function ($response) use (&$packages, $contents, $name, $constraint, $createPackageIfAcceptable) { + if (true === $response) { + $response = $contents; + } + + $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); + foreach ($response['packages'][$name] as $version) { + if (isset($version['versions'])) { + $baseVersion = $version; + foreach ($uniqKeys as $key) { + unset($baseVersion[$key.'s']); + } + + foreach ($version['versions'] as $index => $dummy) { + $unpackedVersion = $baseVersion; + foreach ($uniqKeys as $key) { + $unpackedVersion[$key] = $version[$key.'s'][$index]; + } + + $createPackageIfAcceptable($unpackedVersion, $constraint); + } + } else { + $createPackageIfAcceptable($version, $constraint); + } + } + }, function ($e) { + // TODO use ->done() above instead with react/promise 2.0 + var_dump('Uncaught Ex', $e->getMessage()); + }); + } + } + + $this->httpDownloader->wait(); + + return $packages; + // RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed + } + protected function loadRootServerFile() { if (null !== $this->rootData) { @@ -691,15 +771,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; while ($retries--) { try { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $filename); - if ($this->eventDispatcher) { - $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - } + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - $hostname = parse_url($filename, PHP_URL_HOST) ?: $filename; - $rfs = $preFileDownloadEvent->getRemoteFilesystem(); + $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); - $json = $rfs->getContents($hostname, $filename, false); + $response = $httpDownloader->get($filename); + $json = $response->getBody(); if ($sha256 && $sha256 !== hash('sha256', $json)) { // undo downgrade before trying again if http seems to be hijacked or modifying content somehow if ($this->allowSslDowngrade) { @@ -718,7 +796,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito throw new RepositorySecurityException('The contents of '.$filename.' do not match its signature. This could indicate a man-in-the-middle attack or e.g. antivirus software corrupting files. Try running composer again and report this if you think it is a mistake.'); } - $data = JsonFile::parseJson($json, $filename); + $data = $response->decodeJson(); if (!empty($data['warning'])) { $this->io->writeError('Warning from '.$this->url.': '.$data['warning'].''); } @@ -728,7 +806,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($cacheKey) { if ($storeLastModifiedTime) { - $lastModifiedDate = $rfs->findHeaderValue($rfs->getLastHeaders(), 'last-modified'); + $lastModifiedDate = $response->getHeader('last-modified'); if ($lastModifiedDate) { $data['last-modified'] = $lastModifiedDate; $json = json_encode($data); @@ -737,8 +815,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->cache->write($cacheKey, $json); } + $response->collect(); + break; } catch (\Exception $e) { + if ($e instanceof \LogicException) { + throw $e; + } + if ($e instanceof TransportException && $e->getStatusCode() === 404) { throw $e; } @@ -775,20 +859,18 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; while ($retries--) { try { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $filename); - if ($this->eventDispatcher) { - $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - } + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - $hostname = parse_url($filename, PHP_URL_HOST) ?: $filename; - $rfs = $preFileDownloadEvent->getRemoteFilesystem(); + $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); $options = array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))); - $json = $rfs->getContents($hostname, $filename, false, $options); - if ($json === '' && $rfs->findStatusCode($rfs->getLastHeaders()) === 304) { + $response = $httpDownloader->get($filename, $options); + $json = $response->getBody(); + if ($json === '' && $response->getStatusCode() === 304) { return true; } - $data = JsonFile::parseJson($json, $filename); + $data = $response->decodeJson(); if (!empty($data['warning'])) { $this->io->writeError('Warning from '.$this->url.': '.$data['warning'].''); } @@ -796,7 +878,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->io->writeError('Info from '.$this->url.': '.$data['info'].''); } - $lastModifiedDate = $rfs->findHeaderValue($rfs->getLastHeaders(), 'last-modified'); + $lastModifiedDate = $response->getHeader('last-modified'); + $response->collect(); if ($lastModifiedDate) { $data['last-modified'] = $lastModifiedDate; $json = json_encode($data); @@ -805,6 +888,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $data; } catch (\Exception $e) { + if ($e instanceof \LogicException) { + throw $e; + } + if ($e instanceof TransportException && $e->getStatusCode() === 404) { throw $e; } @@ -825,6 +912,69 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } + protected function asyncFetchFile($filename, $cacheKey, $lastModifiedTime = null) + { + $retries = 3; + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + + $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); + $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); + + $io = $this->io; + $url = $this->url; + $cache = $this->cache; + $degradedMode =& $this->degradedMode; + + $accept = function ($response) use ($io, $url, $cache, $cacheKey) { + $json = $response->getBody(); + if ($json === '' && $response->getStatusCode() === 304) { + return true; + } + + $data = $response->decodeJson(); + if (!empty($data['warning'])) { + $io->writeError('Warning from '.$url.': '.$data['warning'].''); + } + if (!empty($data['info'])) { + $io->writeError('Info from '.$url.': '.$data['info'].''); + } + + $lastModifiedDate = $response->getHeader('last-modified'); + $response->collect(); + if ($lastModifiedDate) { + $data['last-modified'] = $lastModifiedDate; + $json = JsonFile::encode($data, JsonFile::JSON_UNESCAPED_SLASHES | JsonFile::JSON_UNESCAPED_UNICODE); + } + $cache->write($cacheKey, $json); + + return $data; + }; + + $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, $cache, &$degradedMode) { + var_dump('Caught8', $e->getMessage()); + if ($e instanceof TransportException && $e->getStatusCode() === 404) { + return false; + } + + if (--$retries) { + usleep(100000); + + return $httpDownloader->add($filename, $options)->then($accept, $reject); + } + + if (!$degradedMode) { + $io->writeError(''.$e->getMessage().''); + $io->writeError(''.$url.' could not be fully loaded, package information was loaded from the local cache and may be out of date'); + } + $degradedMode = true; + + return true; + }; + + return $httpDownloader->add($filename, $options)->then($accept, $reject); + } + /** * This initializes the packages key of a partial packages.json that contain some packages inlined + a providers-lazy-url * diff --git a/src/Composer/Repository/RepositoryFactory.php b/src/Composer/Repository/RepositoryFactory.php index ca479a7fd..5737a5359 100644 --- a/src/Composer/Repository/RepositoryFactory.php +++ b/src/Composer/Repository/RepositoryFactory.php @@ -16,7 +16,7 @@ use Composer\Factory; use Composer\IO\IOInterface; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Json\JsonFile; /** @@ -108,10 +108,10 @@ class RepositoryFactory * @param IOInterface $io * @param Config $config * @param EventDispatcher $eventDispatcher - * @param RemoteFilesystem $rfs + * @param HttpDownloader $rfs * @return RepositoryManager */ - public static function manager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + public static function manager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, HttpDownloader $rfs = null) { $rm = new RepositoryManager($io, $config, $eventDispatcher, $rfs); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index 87b82d14d..64568514c 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -16,7 +16,7 @@ use Composer\IO\IOInterface; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; /** * Repositories manager. @@ -35,7 +35,7 @@ class RepositoryManager private $eventDispatcher; private $rfs; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, HttpDownloader $rfs = null) { $this->io = $io; $this->config = $config; @@ -127,7 +127,7 @@ class RepositoryManager $reflMethod = new \ReflectionMethod($class, '__construct'); $params = $reflMethod->getParameters(); - if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\RemoteFilesystem') { + if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\HttpDownloader') { return new $class($config, $this->io, $this->config, $this->eventDispatcher, $this->rfs); } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php new file mode 100644 index 000000000..846c41883 --- /dev/null +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -0,0 +1,282 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +use Composer\Config; +use Composer\IO\IOInterface; +use Composer\Downloader\TransportException; +use Composer\CaBundle\CaBundle; +use Psr\Log\LoggerInterface; +use React\Promise\Promise; + +/** + * @author Jordi Boggiano + * @author Nicolas Grekas + */ +class CurlDownloader +{ + private $multiHandle; + private $shareHandle; + private $jobs = array(); + private $io; + private $selectTimeout = 5.0; + protected $multiErrors = array( + CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'), + CURLM_BAD_EASY_HANDLE => array('CURLM_BAD_EASY_HANDLE', "An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle."), + CURLM_OUT_OF_MEMORY => array('CURLM_OUT_OF_MEMORY', 'You are doomed.'), + CURLM_INTERNAL_ERROR => array('CURLM_INTERNAL_ERROR', 'This can only be returned if libcurl bugs. Please report it to us!') + ); + + private static $options = array( + 'http' => array( + 'method' => CURLOPT_CUSTOMREQUEST, + 'content' => CURLOPT_POSTFIELDS, + 'proxy' => CURLOPT_PROXY, + ), + 'ssl' => array( + 'ciphers' => CURLOPT_SSL_CIPHER_LIST, + 'cafile' => CURLOPT_CAINFO, + 'capath' => CURLOPT_CAPATH, + ), + ); + + private static $timeInfo = array( + 'total_time' => true, + 'namelookup_time' => true, + 'connect_time' => true, + 'pretransfer_time' => true, + 'starttransfer_time' => true, + 'redirect_time' => true, + ); + + public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) + { + $this->io = $io; + + $this->multiHandle = $mh = curl_multi_init(); + if (function_exists('curl_multi_setopt')) { + curl_multi_setopt($mh, CURLMOPT_PIPELINING, /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3); + if (defined('CURLMOPT_MAX_HOST_CONNECTIONS')) { + curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 8); + } + } + + if (function_exists('curl_share_init')) { + $this->shareHandle = $sh = curl_share_init(); + curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); + curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); + curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + } + } + + public function download($resolve, $reject, $origin, $url, $options, $copyTo = null) + { + $ch = curl_init(); + $hd = fopen('php://temp/maxmemory:32768', 'w+b'); + + // TODO auth & other context + // TODO cleanup + + if ($copyTo && !$fd = @fopen($copyTo.'~', 'w+b')) { + // TODO throw here probably? + $copyTo = null; + } + if (!$copyTo) { + $fd = @fopen('php://temp/maxmemory:524288', 'w+b'); + } + + if (!isset($options['http']['header'])) { + $options['http']['header'] = array(); + } + + $headers = array_diff($options['http']['header'], array('Connection: close')); + + // TODO + $degradedMode = false; + if ($degradedMode) { + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + } else { + $headers[] = 'Connection: keep-alive'; + $version = curl_version(); + $features = $version['features']; + if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) { + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); + } + } + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + //curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); // TODO increase + curl_setopt($ch, CURLOPT_WRITEHEADER, $hd); + curl_setopt($ch, CURLOPT_FILE, $fd); + if (function_exists('curl_share_init')) { + curl_setopt($ch, CURLOPT_SHARE, $this->shareHandle); + } + + foreach (self::$options as $type => $curlOptions) { + foreach ($curlOptions as $name => $curlOption) { + if (isset($options[$type][$name])) { + curl_setopt($ch, $curlOption, $options[$type][$name]); + } + } + } + + $progress = array_diff_key(curl_getinfo($ch), self::$timeInfo); + + $this->jobs[(int) $ch] = array( + 'progress' => $progress, + 'ch' => $ch, + //'callback' => $params['notification'], + 'file' => $copyTo, + 'hd' => $hd, + 'fd' => $fd, + 'resolve' => $resolve, + 'reject' => $reject, + ); + + $this->io->write('Downloading '.$url, true, IOInterface::DEBUG); + + $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $ch)); + //$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false); + } + + public function tick() + { + // TODO check we have active handles before doing this + if (!$this->jobs) { + return; + } + + $active = true; + try { + $this->checkCurlResult(curl_multi_exec($this->multiHandle, $active)); + if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) { + // sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select + usleep(150); + } + + while ($progress = curl_multi_info_read($this->multiHandle)) { + $h = $progress['handle']; + $i = (int) $h; + if (!isset($this->jobs[$i])) { + continue; + } + $progress = array_diff_key(curl_getinfo($h), self::$timeInfo); + $job = $this->jobs[$i]; + unset($this->jobs[$i]); + curl_multi_remove_handle($this->multiHandle, $h); + $error = curl_error($h); + $errno = curl_errno($h); + curl_close($h); + + try { + //$this->onProgress($h, $job['callback'], $progress, $job['progress']); + if ('' !== $error) { + throw new TransportException(curl_error($h)); + } + + if ($job['file']) { + if (CURLE_OK === $errno) { + fclose($job['fd']); + rename($job['file'].'~', $job['file']); + call_user_func($job['resolve'], true); + } + // TODO otherwise show error? + } else { + rewind($job['hd']); + $headers = explode("\r\n", rtrim(stream_get_contents($job['hd']))); + fclose($job['hd']); + rewind($job['fd']); + $contents = stream_get_contents($job['fd']); + fclose($job['fd']); + $this->io->writeError('['.$progress['http_code'].'] '.$progress['url'], true, IOInterface::DEBUG); + call_user_func($job['resolve'], new Response(array('url' => $progress['url']), $progress['http_code'], $headers, $contents)); + } + } catch (TransportException $e) { + fclose($job['hd']); + fclose($job['fd']); + if ($job['file']) { + @unlink($job['file'].'~'); + } + call_user_func($job['reject'], $e); + } + } + + foreach ($this->jobs as $i => $h) { + if (!isset($this->jobs[$i])) { + continue; + } + $h = $this->jobs[$i]['ch']; + $progress = array_diff_key(curl_getinfo($h), self::$timeInfo); + + if ($this->jobs[$i]['progress'] !== $progress) { + $previousProgress = $this->jobs[$i]['progress']; + $this->jobs[$i]['progress'] = $progress; + try { + //$this->onProgress($h, $this->jobs[$i]['callback'], $progress, $previousProgress); + } catch (TransportException $e) { + var_dump('Caught '.$e->getMessage());die; + unset($this->jobs[$i]); + curl_multi_remove_handle($this->multiHandle, $h); + curl_close($h); + + fclose($job['hd']); + fclose($job['fd']); + if ($job['file']) { + @unlink($job['file'].'~'); + } + call_user_func($job['reject'], $e); + } + } + } + } catch (\Exception $e) { + var_dump('Caught2', get_class($e), $e->getMessage(), $e);die; + } + +// TODO finalize / resolve +// if ($copyTo && !isset($this->exceptions[(int) $ch])) { +// $fd = fopen($copyTo, 'rb'); +// } +// + } + + private function onProgress($ch, callable $notify, array $progress, array $previousProgress) + { + if (300 <= $progress['http_code'] && $progress['http_code'] < 400) { + return; + } + if (!$previousProgress['http_code'] && $progress['http_code'] && $progress['http_code'] < 200 || 400 <= $progress['http_code']) { + $code = 403 === $progress['http_code'] ? STREAM_NOTIFY_AUTH_RESULT : STREAM_NOTIFY_FAILURE; + $notify($code, STREAM_NOTIFY_SEVERITY_ERR, curl_error($ch), $progress['http_code'], 0, 0, false); + } + if ($previousProgress['download_content_length'] < $progress['download_content_length']) { + $notify(STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['download_content_length'], false); + } + if ($previousProgress['size_download'] < $progress['size_download']) { + $notify(STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, (int) $progress['size_download'], (int) $progress['download_content_length'], false); + } + } + + private function checkCurlResult($code) + { + if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) { + throw new \RuntimeException(isset($this->multiErrors[$code]) + ? "cURL error: {$code} ({$this->multiErrors[$code][0]}): cURL message: {$this->multiErrors[$code][1]}" + : 'Unexpected cURL error: ' . $code + ); + } + } +} diff --git a/src/Composer/Util/Http/Response.php b/src/Composer/Util/Http/Response.php new file mode 100644 index 000000000..ff48fdb40 --- /dev/null +++ b/src/Composer/Util/Http/Response.php @@ -0,0 +1,75 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +use Composer\Json\JsonFile; + +class Response +{ + private $request; + private $code; + private $headers; + private $body; + + public function __construct(array $request, $code, array $headers, $body) + { + $this->request = $request; + $this->code = $code; + $this->headers = $headers; + $this->body = $body; + } + + public function getStatusCode() + { + return $this->code; + } + + public function getHeaders() + { + return $this->headers; + } + + public function getHeader($name) + { + $value = null; + foreach ($this->headers as $header) { + if (preg_match('{^'.$name.':\s*(.+?)\s*$}i', $header, $match)) { + $value = $match[1]; + } elseif (preg_match('{^HTTP/}i', $header)) { + // TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary + // + // In case of redirects, http_response_headers contains the headers of all responses + // so we reset the flag when a new response is being parsed as we are only interested in the last response + $value = null; + } + } + + return $value; + } + + + public function getBody() + { + return $this->body; + } + + public function decodeJson() + { + return JsonFile::parseJson($this->body, $this->request['url']); + } + + public function collect() + { + $this->request = $this->code = $this->headers = $this->body = null; + } +} diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php new file mode 100644 index 000000000..31c615e0c --- /dev/null +++ b/src/Composer/Util/HttpDownloader.php @@ -0,0 +1,246 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Composer\Config; +use Composer\IO\IOInterface; +use Composer\Downloader\TransportException; +use Composer\CaBundle\CaBundle; +use Psr\Log\LoggerInterface; +use React\Promise\Promise; + +/** + * @author Jordi Boggiano + */ +class HttpDownloader +{ + const STATUS_QUEUED = 1; + const STATUS_STARTED = 2; + const STATUS_COMPLETED = 3; + const STATUS_FAILED = 4; + + private $io; + private $config; + private $jobs = array(); + private $index; + private $progress; + private $lastProgress; + private $disableTls = false; + private $curl; + private $rfs; + private $idGen = 0; + + /** + * Constructor. + * + * @param IOInterface $io The IO instance + * @param Config $config The config + * @param array $options The options + * @param bool $disableTls + */ + public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) + { + $this->io = $io; + + // Setup TLS options + // The cafile option can be set via config.json + if ($disableTls === false) { + $logger = $io instanceof LoggerInterface ? $io : null; + $this->options = StreamContextFactory::getTlsDefaults($options, $logger); + } else { + $this->disableTls = true; + } + + // handle the other externally set options normally. + $this->options = array_replace_recursive($this->options, $options); + $this->config = $config; + + if (extension_loaded('curl')) { + $this->curl = new Http\CurlDownloader($io, $config, $options, $disableTls); + } + + $this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls); + } + + public function get($url, $options = array()) + { + list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false), true); + $this->wait($job['id']); + + return $this->getResponse($job['id']); + } + + public function add($url, $options = array()) + { + list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false)); + + return $promise; + } + + public function copy($url, $to, $options = array()) + { + list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to), true); + $this->wait($job['id']); + + return $this->getResponse($job['id']); + } + + public function addCopy($url, $to, $options = array()) + { + list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to)); + + return $promise; + } + + private function addJob($request, $sync = false) + { + $job = array( + 'id' => $this->idGen++, + 'status' => self::STATUS_QUEUED, + 'request' => $request, + 'sync' => $sync, + ); + + $curl = $this->curl; + $rfs = $this->rfs; + $io = $this->io; + + $origin = $this->getOrigin($job['request']['url']); + + // TODO only send http/https through curl + if ($curl) { + $resolver = function ($resolve, $reject) use (&$job, $curl, $origin) { + // start job + $url = $job['request']['url']; + $options = $job['request']['options']; + + $job['status'] = HttpDownloader::STATUS_STARTED; + + if ($job['request']['copyTo']) { + $curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); + } else { + $curl->download($resolve, $reject, $origin, $url, $options); + } + }; + } else { + $resolver = function ($resolve, $reject) use (&$job, $rfs, $curl, $origin) { + // start job + $url = $job['request']['url']; + $options = $job['request']['options']; + + $job['status'] = HttpDownloader::STATUS_STARTED; + + if ($job['request']['copyTo']) { + if ($curl) { + $result = $curl->download($origin, $url, $options, $job['request']['copyTo']); + } else { + $result = $rfs->copy($origin, $url, $job['request']['copyTo'], false /* TODO progress */, $options); + } + + $resolve($result); + } else { + $body = $rfs->getContents($origin, $url, false /* TODO progress */, $options); + $headers = $rfs->getLastHeaders(); + $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $body); + + $resolve($response); + } + }; + } + + $canceler = function () {}; + + $promise = new Promise($resolver, $canceler); + $promise->then(function ($response) use (&$job) { + $job['status'] = HttpDownloader::STATUS_COMPLETED; + $job['response'] = $response; + // TODO look for more jobs to start once we throttle to max X jobs + }, function ($e) use ($io, &$job) { + var_dump(__CLASS__ . __LINE__); + var_dump(gettype($e)); + var_dump($e->getMessage()); + die; + $job['status'] = HttpDownloader::STATUS_FAILED; + $job['exception'] = $e; + }); + $this->jobs[$job['id']] =& $job; + + return array($job, $promise); + } + + public function wait($index = null, $progress = false) + { + while (true) { + if ($this->curl) { + $this->curl->tick(); + } + + if (null !== $index) { + if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) { + return; + } + } else { + $done = true; + foreach ($this->jobs as $job) { + if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) { + $done = false; + break; + } elseif (!$job['sync']) { + unset($this->jobs[$job['id']]); + } + } + if ($done) { + return; + } + } + + usleep(1000); + } + } + + private function getResponse($index) + { + if (!isset($this->jobs[$index])) { + throw new \LogicException('Invalid request id'); + } + + if ($this->jobs[$index]['status'] === self::STATUS_FAILED) { + throw $this->jobs[$index]['exception']; + } + + if (!isset($this->jobs[$index]['response'])) { + throw new \LogicException('Response not available yet, call wait() first'); + } + + $resp = $this->jobs[$index]['response']; + + unset($this->jobs[$index]); + + return $resp; + } + + private function getOrigin($url) + { + $origin = parse_url($url, PHP_URL_HOST); + + if ($origin === 'api.github.com') { + return 'github.com'; + } + + if ($origin === 'repo.packagist.org') { + return 'packagist.org'; + } + + return $origin ?: $url; + } +} diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index ea18a9e30..f4a211acb 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -60,7 +60,8 @@ class RemoteFilesystem // Setup TLS options // The cafile option can be set via config.json if ($disableTls === false) { - $this->options = $this->getTlsDefaults($options); + $logger = $io instanceof LoggerInterface ? $io : null; + $this->options = StreamContextFactory::getTlsDefaults($options, $logger); } else { $this->disableTls = true; } @@ -891,111 +892,6 @@ class RemoteFilesystem return false; } - /** - * @param array $options - * - * @return array - */ - private function getTlsDefaults(array $options) - { - $ciphers = implode(':', array( - 'ECDHE-RSA-AES128-GCM-SHA256', - 'ECDHE-ECDSA-AES128-GCM-SHA256', - 'ECDHE-RSA-AES256-GCM-SHA384', - 'ECDHE-ECDSA-AES256-GCM-SHA384', - 'DHE-RSA-AES128-GCM-SHA256', - 'DHE-DSS-AES128-GCM-SHA256', - 'kEDH+AESGCM', - 'ECDHE-RSA-AES128-SHA256', - 'ECDHE-ECDSA-AES128-SHA256', - 'ECDHE-RSA-AES128-SHA', - 'ECDHE-ECDSA-AES128-SHA', - 'ECDHE-RSA-AES256-SHA384', - 'ECDHE-ECDSA-AES256-SHA384', - 'ECDHE-RSA-AES256-SHA', - 'ECDHE-ECDSA-AES256-SHA', - 'DHE-RSA-AES128-SHA256', - 'DHE-RSA-AES128-SHA', - 'DHE-DSS-AES128-SHA256', - 'DHE-RSA-AES256-SHA256', - 'DHE-DSS-AES256-SHA', - 'DHE-RSA-AES256-SHA', - 'AES128-GCM-SHA256', - 'AES256-GCM-SHA384', - 'AES128-SHA256', - 'AES256-SHA256', - 'AES128-SHA', - 'AES256-SHA', - 'AES', - 'CAMELLIA', - 'DES-CBC3-SHA', - '!aNULL', - '!eNULL', - '!EXPORT', - '!DES', - '!RC4', - '!MD5', - '!PSK', - '!aECDH', - '!EDH-DSS-DES-CBC3-SHA', - '!EDH-RSA-DES-CBC3-SHA', - '!KRB5-DES-CBC3-SHA', - )); - - /** - * CN_match and SNI_server_name are only known once a URL is passed. - * They will be set in the getOptionsForUrl() method which receives a URL. - * - * cafile or capath can be overridden by passing in those options to constructor. - */ - $defaults = array( - 'ssl' => array( - 'ciphers' => $ciphers, - 'verify_peer' => true, - 'verify_depth' => 7, - 'SNI_enabled' => true, - 'capture_peer_cert' => true, - ), - ); - - if (isset($options['ssl'])) { - $defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']); - } - - $caBundleLogger = $this->io instanceof LoggerInterface ? $this->io : null; - - /** - * Attempt to find a local cafile or throw an exception if none pre-set - * The user may go download one if this occurs. - */ - if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) { - $result = CaBundle::getSystemCaRootBundlePath($caBundleLogger); - - if (is_dir($result)) { - $defaults['ssl']['capath'] = $result; - } else { - $defaults['ssl']['cafile'] = $result; - } - } - - if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $caBundleLogger))) { - throw new TransportException('The configured cafile was not valid or could not be read.'); - } - - if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) { - throw new TransportException('The configured capath was not valid or could not be read.'); - } - - /** - * Disable TLS compression to prevent CRIME attacks where supported. - */ - if (PHP_VERSION_ID >= 50413) { - $defaults['ssl']['disable_compression'] = true; - } - - return $defaults; - } - /** * Fetch certificate common name and fingerprint for validation of SAN. * diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 8dfd6624a..72d12115d 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -13,6 +13,8 @@ namespace Composer\Util; use Composer\Composer; +use Composer\CaBundle\CaBundle; +use Psr\Log\LoggerInterface; /** * Allows the creation of a basic context supporting http proxy @@ -153,6 +155,109 @@ final class StreamContextFactory return stream_context_create($options, $defaultParams); } + /** + * @param array $options + * + * @return array + */ + public static function getTlsDefaults(array $options, LoggerInterface $logger = null) + { + $ciphers = implode(':', array( + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'DHE-RSA-AES128-GCM-SHA256', + 'DHE-DSS-AES128-GCM-SHA256', + 'kEDH+AESGCM', + 'ECDHE-RSA-AES128-SHA256', + 'ECDHE-ECDSA-AES128-SHA256', + 'ECDHE-RSA-AES128-SHA', + 'ECDHE-ECDSA-AES128-SHA', + 'ECDHE-RSA-AES256-SHA384', + 'ECDHE-ECDSA-AES256-SHA384', + 'ECDHE-RSA-AES256-SHA', + 'ECDHE-ECDSA-AES256-SHA', + 'DHE-RSA-AES128-SHA256', + 'DHE-RSA-AES128-SHA', + 'DHE-DSS-AES128-SHA256', + 'DHE-RSA-AES256-SHA256', + 'DHE-DSS-AES256-SHA', + 'DHE-RSA-AES256-SHA', + 'AES128-GCM-SHA256', + 'AES256-GCM-SHA384', + 'AES128-SHA256', + 'AES256-SHA256', + 'AES128-SHA', + 'AES256-SHA', + 'AES', + 'CAMELLIA', + 'DES-CBC3-SHA', + '!aNULL', + '!eNULL', + '!EXPORT', + '!DES', + '!RC4', + '!MD5', + '!PSK', + '!aECDH', + '!EDH-DSS-DES-CBC3-SHA', + '!EDH-RSA-DES-CBC3-SHA', + '!KRB5-DES-CBC3-SHA', + )); + + /** + * CN_match and SNI_server_name are only known once a URL is passed. + * They will be set in the getOptionsForUrl() method which receives a URL. + * + * cafile or capath can be overridden by passing in those options to constructor. + */ + $defaults = array( + 'ssl' => array( + 'ciphers' => $ciphers, + 'verify_peer' => true, + 'verify_depth' => 7, + 'SNI_enabled' => true, + 'capture_peer_cert' => true, + ), + ); + + if (isset($options['ssl'])) { + $defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']); + } + + /** + * Attempt to find a local cafile or throw an exception if none pre-set + * The user may go download one if this occurs. + */ + if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) { + $result = CaBundle::getSystemCaRootBundlePath($logger); + + if (is_dir($result)) { + $defaults['ssl']['capath'] = $result; + } else { + $defaults['ssl']['cafile'] = $result; + } + } + + if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $logger))) { + throw new TransportException('The configured cafile was not valid or could not be read.'); + } + + if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) { + throw new TransportException('The configured capath was not valid or could not be read.'); + } + + /** + * Disable TLS compression to prevent CRIME attacks where supported. + */ + if (PHP_VERSION_ID >= 50413) { + $defaults['ssl']['disable_compression'] = true; + } + + return $defaults; + } + /** * A bug in PHP prevents the headers from correctly being sent when a content-type header is present and * NOT at the end of the array From 713bc4de1d506a83f341e033569ded22bff7d228 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 31 Oct 2018 12:44:54 +0100 Subject: [PATCH 026/321] Minor fixes and updated the rest of the code/tests to use HttpDownloader --- doc/articles/plugins.md | 4 +- src/Composer/Command/ArchiveCommand.php | 2 +- src/Composer/Downloader/FileDownloader.php | 4 +- src/Composer/Downloader/GzipDownloader.php | 4 +- src/Composer/Downloader/RarDownloader.php | 4 +- src/Composer/Downloader/XzDownloader.php | 4 +- src/Composer/Downloader/ZipDownloader.php | 4 +- src/Composer/Factory.php | 32 +++--- src/Composer/Package/Loader/ArrayLoader.php | 3 +- .../Repository/ComposerRepository.php | 17 ++- .../Repository/Pear/BaseChannelReader.php | 16 ++- .../Repository/Pear/ChannelReader.php | 10 +- .../Repository/Pear/ChannelRest10Reader.php | 5 +- .../Repository/Pear/ChannelRest11Reader.php | 6 +- src/Composer/Repository/PearRepository.php | 10 +- src/Composer/Repository/RepositoryFactory.php | 6 +- src/Composer/Repository/RepositoryManager.php | 8 +- .../Repository/Vcs/BitbucketDriver.php | 25 +++-- .../Repository/Vcs/GitBitbucketDriver.php | 2 +- src/Composer/Repository/Vcs/GitHubDriver.php | 53 +++++---- src/Composer/Repository/Vcs/GitLabDriver.php | 55 ++++----- .../Repository/Vcs/HgBitbucketDriver.php | 2 +- src/Composer/Repository/Vcs/VcsDriver.php | 17 +-- src/Composer/Util/Bitbucket.php | 12 +- src/Composer/Util/GitHub.php | 10 +- src/Composer/Util/GitLab.php | 12 +- src/Composer/Util/Http/Response.php | 3 + src/Composer/Util/HttpDownloader.php | 10 +- src/Composer/Util/RemoteFilesystem.php | 4 +- .../Test/Downloader/ArchiveDownloaderTest.php | 6 +- .../Test/Downloader/FileDownloaderTest.php | 6 +- .../Test/Downloader/XzDownloaderTest.php | 4 +- .../Test/Downloader/ZipDownloaderTest.php | 38 +++---- tests/Composer/Test/InstallerTest.php | 5 +- ...esystemMock.php => HttpDownloaderMock.php} | 12 +- .../Package/Archiver/ArchiveManagerTest.php | 9 +- .../Repository/ComposerRepositoryTest.php | 32 ++++-- .../Test/Repository/PathRepositoryTest.php | 2 +- .../Repository/Pear/ChannelReaderTest.php | 10 +- .../Pear/ChannelRest10ReaderTest.php | 4 +- .../Pear/ChannelRest11ReaderTest.php | 4 +- .../Test/Repository/PearRepositoryTest.php | 4 +- .../Test/Repository/RepositoryFactoryTest.php | 4 +- .../Test/Repository/RepositoryManagerTest.php | 6 +- .../Repository/Vcs/GitBitbucketDriverTest.php | 68 ++++++----- .../Test/Repository/Vcs/GitHubDriverTest.php | 85 +++++++------- .../Test/Repository/Vcs/GitLabDriverTest.php | 106 +++++++----------- .../Repository/Vcs/PerforceDriverTest.php | 14 +-- tests/Composer/Test/Util/BitbucketTest.php | 72 ++++++------ tests/Composer/Test/Util/GitHubTest.php | 31 +++-- tests/Composer/Test/Util/GitLabTest.php | 31 +++-- 51 files changed, 461 insertions(+), 436 deletions(-) rename tests/Composer/Test/Mock/{RemoteFilesystemMock.php => HttpDownloaderMock.php} (73%) diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index 228cbac9e..59e2a2f15 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -176,8 +176,8 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface if ($protocol === 's3') { $awsClient = new AwsClient($this->io, $this->composer->getConfig()); - $s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient); - $event->setRemoteFilesystem($s3RemoteFilesystem); + $s3Downloader = new S3Downloader($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient); + $event->setHttpdownloader($s3Downloader); } } } diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index 29858c6fc..f893ed679 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -104,7 +104,7 @@ EOT $archiveManager = $composer->getArchiveManager(); } else { $factory = new Factory; - $downloadManager = $factory->createDownloadManager($io, $config); + $downloadManager = $factory->createDownloadManager($io, $config, $factory->createHttpDownloader($io, $config)); $archiveManager = $factory->createArchiveManager($config, $downloadManager); } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 6b1349bb8..8b196e60c 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -51,12 +51,12 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface * * @param IOInterface $io The IO instance * @param Config $config The config + * @param HttpDownloader $httpDownloader The remote filesystem * @param EventDispatcher $eventDispatcher The event dispatcher * @param Cache $cache Cache instance - * @param HttpDownloader $httpDownloader The remote filesystem * @param Filesystem $filesystem The filesystem */ - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher, Cache $cache, HttpDownloader $httpDownloader, Filesystem $filesystem = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null) { $this->io = $io; $this->config = $config; diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index bb86f6267..f65fcf27d 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -30,10 +30,10 @@ class GzipDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, HttpDownloader $downloader = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $downloader); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } protected function extract($file, $path) diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index c1ed8d60b..6fe4cf27c 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -33,10 +33,10 @@ class RarDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, HttpDownloader $downloader = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $downloader); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } protected function extract($file, $path) diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 1f5947997..bd7b028e2 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -30,11 +30,11 @@ class XzDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, HttpDownloader $downloader = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $downloader); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } protected function extract($file, $path) diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 10518e026..9eceab250 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -36,10 +36,10 @@ class ZipDownloader extends ArchiveDownloader protected $process; private $zipArchiveObject; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, HttpDownloader $downloader = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $eventDispatcher, $cache, $downloader); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } /** diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 97d507b10..00aa499d0 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -325,14 +325,14 @@ class Factory $io->loadConfiguration($config); } - $rfs = self::createHttpDownloader($io, $config); + $httpDownloader = self::createHttpDownloader($io, $config); // initialize event dispatcher $dispatcher = new EventDispatcher($composer, $io); $composer->setEventDispatcher($dispatcher); // initialize repository manager - $rm = RepositoryFactory::manager($io, $config, $dispatcher, $rfs); + $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher); $composer->setRepositoryManager($rm); // load local repository @@ -357,7 +357,7 @@ class Factory if ($fullLoad) { // initialize download manager - $dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs); + $dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher); $composer->setDownloadManager($dm); // initialize autoload generator @@ -451,7 +451,7 @@ class Factory * @param EventDispatcher $eventDispatcher * @return Downloader\DownloadManager */ - public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, HttpDownloader $rfs = null) + public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { $cache = null; if ($config->get('cache-files-ttl') > 0) { @@ -484,14 +484,14 @@ class Factory $dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs)); $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config)); - $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); - $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); - $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs)); - $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); - $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); - $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs)); - $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs)); - $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs)); + $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); + $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); + $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); + $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); + $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); + $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); + $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); + $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); return $dm; } @@ -501,14 +501,8 @@ class Factory * @param Downloader\DownloadManager $dm Manager use to download sources * @return Archiver\ArchiveManager */ - public function createArchiveManager(Config $config, Downloader\DownloadManager $dm = null) + public function createArchiveManager(Config $config, Downloader\DownloadManager $dm) { - if (null === $dm) { - $io = new IO\NullIO(); - $io->loadConfiguration($config); - $dm = $this->createDownloadManager($io, $config); - } - $am = new Archiver\ArchiveManager($dm); $am->addArchiver(new Archiver\ZipArchiver); $am->addArchiver(new Archiver\PharArchiver); diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 303cc3c13..49ba45aa8 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -18,7 +18,6 @@ use Composer\Package\Link; use Composer\Package\RootAliasPackage; use Composer\Package\RootPackageInterface; use Composer\Package\Version\VersionParser; -use Composer\Semver\VersionParser as SemverVersionParser; /** * @author Konstantin Kudryashiv @@ -29,7 +28,7 @@ class ArrayLoader implements LoaderInterface protected $versionParser; protected $loadOptions; - public function __construct(SemverVersionParser $parser = null, $loadOptions = false) + public function __construct(VersionParser $parser = null, $loadOptions = false) { if (!$parser) { $parser = new VersionParser; diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index c4a570c75..a0036e4fd 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -102,7 +102,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->versionParser = new VersionParser(); $this->loader = new ArrayLoader($this->versionParser); if ($httpDownloader && $this->options) { - // TODO solve this somehow - should be sent a request time not on the instance + // TODO solve this somehow - should be sent at request time not on the instance $httpDownloader = clone $httpDownloader; $httpDownloader->setOptions($this->options); } @@ -543,6 +543,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $response = $contents; } + if (!isset($response['packages'][$name])) { + return; + } + $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); foreach ($response['packages'][$name] as $version) { if (isset($version['versions'])) { @@ -566,6 +570,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito }, function ($e) { // TODO use ->done() above instead with react/promise 2.0 var_dump('Uncaught Ex', $e->getMessage()); + throw $e; }); } } @@ -644,6 +649,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasProviders = true; } + // TODO this is for testing only, remove once packagist reports v2 protocol support + if (preg_match('{^https?://repo\.packagist\.org/?$}i', $this->url)) { + $this->repoConfig['force-lazy-providers'] = true; + } + // force values for packagist if (preg_match('{^https?://repo\.packagist\.org/?$}i', $this->url) && !empty($this->repoConfig['force-lazy-providers'])) { $this->url = 'https://repo.packagist.org'; @@ -927,6 +937,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $degradedMode =& $this->degradedMode; $accept = function ($response) use ($io, $url, $cache, $cacheKey) { + // package not found is acceptable for a v2 protocol repository + if ($response->getStatusCode() === 404) { + return array('packages' => array()); + } + $json = $response->getBody(); if ($json === '' && $response->getStatusCode() === 304) { return true; diff --git a/src/Composer/Repository/Pear/BaseChannelReader.php b/src/Composer/Repository/Pear/BaseChannelReader.php index 9b26eb9db..b778bf08b 100644 --- a/src/Composer/Repository/Pear/BaseChannelReader.php +++ b/src/Composer/Repository/Pear/BaseChannelReader.php @@ -12,7 +12,7 @@ namespace Composer\Repository\Pear; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; /** * Base PEAR Channel reader. @@ -33,12 +33,12 @@ abstract class BaseChannelReader const ALL_RELEASES_NS = 'http://pear.php.net/dtd/rest.allreleases'; const PACKAGE_INFO_NS = 'http://pear.php.net/dtd/rest.package'; - /** @var RemoteFilesystem */ - private $rfs; + /** @var HttpDownloader */ + private $httpDownloader; - protected function __construct(RemoteFilesystem $rfs) + protected function __construct(HttpDownloader $httpDownloader) { - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; } /** @@ -52,7 +52,11 @@ abstract class BaseChannelReader protected function requestContent($origin, $path) { $url = rtrim($origin, '/') . '/' . ltrim($path, '/'); - $content = $this->rfs->getContents($origin, $url, false); + try { + $content = $this->httpDownloader->get($url)->getBody(); + } catch (\Exception $e) { + throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.', 0, $e); + } if (!$content) { throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.'); } diff --git a/src/Composer/Repository/Pear/ChannelReader.php b/src/Composer/Repository/Pear/ChannelReader.php index 73cc9152e..14d48ad86 100644 --- a/src/Composer/Repository/Pear/ChannelReader.php +++ b/src/Composer/Repository/Pear/ChannelReader.php @@ -12,7 +12,7 @@ namespace Composer\Repository\Pear; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; /** * PEAR Channel package reader. @@ -26,12 +26,12 @@ class ChannelReader extends BaseChannelReader /** @var array of ('xpath test' => 'rest implementation') */ private $readerMap; - public function __construct(RemoteFilesystem $rfs) + public function __construct(HttpDownloader $httpDownloader) { - parent::__construct($rfs); + parent::__construct($httpDownloader); - $rest10reader = new ChannelRest10Reader($rfs); - $rest11reader = new ChannelRest11Reader($rfs); + $rest10reader = new ChannelRest10Reader($httpDownloader); + $rest11reader = new ChannelRest11Reader($httpDownloader); $this->readerMap = array( 'REST1.3' => $rest11reader, diff --git a/src/Composer/Repository/Pear/ChannelRest10Reader.php b/src/Composer/Repository/Pear/ChannelRest10Reader.php index 489914d5d..93969043a 100644 --- a/src/Composer/Repository/Pear/ChannelRest10Reader.php +++ b/src/Composer/Repository/Pear/ChannelRest10Reader.php @@ -13,6 +13,7 @@ namespace Composer\Repository\Pear; use Composer\Downloader\TransportException; +use Composer\Util\HttpDownloader; /** * Read PEAR packages using REST 1.0 interface @@ -29,9 +30,9 @@ class ChannelRest10Reader extends BaseChannelReader { private $dependencyReader; - public function __construct($rfs) + public function __construct(HttpDownloader $httpDownloader) { - parent::__construct($rfs); + parent::__construct($httpDownloader); $this->dependencyReader = new PackageDependencyParser(); } diff --git a/src/Composer/Repository/Pear/ChannelRest11Reader.php b/src/Composer/Repository/Pear/ChannelRest11Reader.php index f9e05f5be..18b1b10f3 100644 --- a/src/Composer/Repository/Pear/ChannelRest11Reader.php +++ b/src/Composer/Repository/Pear/ChannelRest11Reader.php @@ -12,6 +12,8 @@ namespace Composer\Repository\Pear; +use Composer\Util\HttpDownloader; + /** * Read PEAR packages using REST 1.1 interface * @@ -25,9 +27,9 @@ class ChannelRest11Reader extends BaseChannelReader { private $dependencyReader; - public function __construct($rfs) + public function __construct(HttpDownloader $httpDownloader) { - parent::__construct($rfs); + parent::__construct($httpDownloader); $this->dependencyReader = new PackageDependencyParser(); } diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index c4f0b83e7..aef5c0381 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -21,7 +21,7 @@ use Composer\Repository\Pear\ChannelInfo; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Config; use Composer\Factory; @@ -38,7 +38,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn { private $url; private $io; - private $rfs; + private $httpDownloader; private $versionParser; private $repoConfig; @@ -47,7 +47,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn */ private $vendorAlias; - public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher = null, RemoteFilesystem $rfs = null) + public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher, HttpDownloader $httpDownloader) { parent::__construct(); if (!preg_match('{^https?://}', $repoConfig['url'])) { @@ -61,7 +61,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn $this->url = rtrim($repoConfig['url'], '/'); $this->io = $io; - $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader; $this->vendorAlias = isset($repoConfig['vendor-alias']) ? $repoConfig['vendor-alias'] : null; $this->versionParser = new VersionParser(); $this->repoConfig = $repoConfig; @@ -78,7 +78,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn $this->io->writeError('Initializing PEAR repository '.$this->url); - $reader = new ChannelReader($this->rfs); + $reader = new ChannelReader($this->httpDownloader); try { $channelInfo = $reader->read($this->url); } catch (\Exception $e) { diff --git a/src/Composer/Repository/RepositoryFactory.php b/src/Composer/Repository/RepositoryFactory.php index 5737a5359..515908f64 100644 --- a/src/Composer/Repository/RepositoryFactory.php +++ b/src/Composer/Repository/RepositoryFactory.php @@ -108,12 +108,12 @@ class RepositoryFactory * @param IOInterface $io * @param Config $config * @param EventDispatcher $eventDispatcher - * @param HttpDownloader $rfs + * @param HttpDownloader $httpDownloader * @return RepositoryManager */ - public static function manager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, HttpDownloader $rfs = null) + public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { - $rm = new RepositoryManager($io, $config, $eventDispatcher, $rfs); + $rm = new RepositoryManager($io, $config, $eventDispatcher, $httpDownloader); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index 64568514c..8fc01cb08 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -33,14 +33,14 @@ class RepositoryManager private $io; private $config; private $eventDispatcher; - private $rfs; + private $httpDownloader; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, HttpDownloader $rfs = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher, HttpDownloader $httpDownloader) { $this->io = $io; $this->config = $config; $this->eventDispatcher = $eventDispatcher; - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; } /** @@ -128,7 +128,7 @@ class RepositoryManager $reflMethod = new \ReflectionMethod($class, '__construct'); $params = $reflMethod->getParameters(); if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\HttpDownloader') { - return new $class($config, $this->io, $this->config, $this->eventDispatcher, $this->rfs); + return new $class($config, $this->io, $this->config, $this->eventDispatcher, $this->httpDownloader); } return new $class($config, $this->io, $this->config, $this->eventDispatcher); diff --git a/src/Composer/Repository/Vcs/BitbucketDriver.php b/src/Composer/Repository/Vcs/BitbucketDriver.php index 24a4af4dd..bde4fc1b7 100644 --- a/src/Composer/Repository/Vcs/BitbucketDriver.php +++ b/src/Composer/Repository/Vcs/BitbucketDriver.php @@ -16,6 +16,7 @@ use Composer\Cache; use Composer\Downloader\TransportException; use Composer\Json\JsonFile; use Composer\Util\Bitbucket; +use Composer\Util\Http\Response; abstract class BitbucketDriver extends VcsDriver { @@ -92,7 +93,7 @@ abstract class BitbucketDriver extends VcsDriver ) ); - $repoData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource, true), $resource); + $repoData = $this->fetchWithOAuthCredentials($resource, true)->decodeJson(); if ($this->fallbackDriver) { return false; } @@ -204,7 +205,7 @@ abstract class BitbucketDriver extends VcsDriver $file ); - return $this->getContentsWithOAuthCredentials($resource); + return $this->fetchWithOAuthCredentials($resource)->getBody(); } /** @@ -222,7 +223,7 @@ abstract class BitbucketDriver extends VcsDriver $this->repository, $identifier ); - $commit = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + $commit = $this->fetchWithOAuthCredentials($resource)->decodeJson(); return new \DateTime($commit['date']); } @@ -284,7 +285,7 @@ abstract class BitbucketDriver extends VcsDriver ); $hasNext = true; while ($hasNext) { - $tagsData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + $tagsData = $this->fetchWithOAuthCredentials($resource)->decodeJson(); foreach ($tagsData['values'] as $data) { $this->tags[$data['name']] = $data['target']['hash']; } @@ -328,7 +329,7 @@ abstract class BitbucketDriver extends VcsDriver ); $hasNext = true; while ($hasNext) { - $branchData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + $branchData = $this->fetchWithOAuthCredentials($resource)->decodeJson(); foreach ($branchData['values'] as $data) { // skip headless branches which seem to be deleted branches that bitbucket nevertheless returns in the API if ($this->vcsType === 'hg' && empty($data['heads'])) { @@ -354,14 +355,14 @@ abstract class BitbucketDriver extends VcsDriver * @param string $url The URL of content * @param bool $fetchingRepoData * - * @return mixed The result + * @return Response The result */ - protected function getContentsWithOAuthCredentials($url, $fetchingRepoData = false) + protected function fetchWithOAuthCredentials($url, $fetchingRepoData = false) { try { return parent::getContents($url); } catch (TransportException $e) { - $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->remoteFilesystem); + $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->httpDownloader); if (403 === $e->getCode() || (401 === $e->getCode() && strpos($e->getMessage(), 'Could not authenticate against') === 0)) { if (!$this->io->hasAuthentication($this->originUrl) @@ -371,7 +372,9 @@ abstract class BitbucketDriver extends VcsDriver } if (!$this->io->isInteractive() && $fetchingRepoData) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } } @@ -390,6 +393,8 @@ abstract class BitbucketDriver extends VcsDriver { try { $this->setupFallbackDriver($this->generateSshUrl()); + + return true; } catch (\RuntimeException $e) { $this->fallbackDriver = null; @@ -433,7 +438,7 @@ abstract class BitbucketDriver extends VcsDriver $this->repository ); - $data = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + $data = $this->fetchWithOAuthCredentials($resource)->decodeJson(); if (isset($data['mainbranch'])) { return $data['mainbranch']; } diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index c8a5c9905..dd69e753a 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -76,7 +76,7 @@ class GitBitbucketDriver extends BitbucketDriver $this->io, $this->config, $this->process, - $this->remoteFilesystem + $this->httpDownloader ); $this->fallbackDriver->initialize(); } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index d0b721af9..f1ad253d8 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -18,6 +18,8 @@ use Composer\Json\JsonFile; use Composer\Cache; use Composer\IO\IOInterface; use Composer\Util\GitHub; +use Composer\Util\Http\Response; +use Composer\Util\RemoteFilesystem; /** * @author Jordi Boggiano @@ -184,7 +186,7 @@ class GitHubDriver extends VcsDriver } $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/' . $file . '?ref='.urlencode($identifier); - $resource = JsonFile::parseJson($this->getContents($resource)); + $resource = $this->getContents($resource)->decodeJson(); if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) { throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier); } @@ -202,7 +204,7 @@ class GitHubDriver extends VcsDriver } $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); - $commit = JsonFile::parseJson($this->getContents($resource), $resource); + $commit = $this->getContents($resource)->decodeJson(); return new \DateTime($commit['commit']['committer']['date']); } @@ -220,12 +222,13 @@ class GitHubDriver extends VcsDriver $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags?per_page=100'; do { - $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); + $response = $this->getContents($resource); + $tagsData = $response->decodeJson(); foreach ($tagsData as $tag) { $this->tags[$tag['name']] = $tag['commit']['sha']; } - $resource = $this->getNextPage(); + $resource = $this->getNextPage($response); } while ($resource); } @@ -247,7 +250,8 @@ class GitHubDriver extends VcsDriver $branchBlacklist = array('gh-pages'); do { - $branchData = JsonFile::parseJson($this->getContents($resource), $resource); + $response = $this->getContents($resource); + $branchData = $response->decodeJson(); foreach ($branchData as $branch) { $name = substr($branch['ref'], 11); if (!in_array($name, $branchBlacklist)) { @@ -255,7 +259,7 @@ class GitHubDriver extends VcsDriver } } - $resource = $this->getNextPage(); + $resource = $this->getNextPage($response); } while ($resource); } @@ -315,7 +319,7 @@ class GitHubDriver extends VcsDriver try { return parent::getContents($url); } catch (TransportException $e) { - $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->remoteFilesystem); + $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->httpDownloader); switch ($e->getCode()) { case 401: @@ -330,16 +334,18 @@ class GitHubDriver extends VcsDriver } if (!$this->io->isInteractive()) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } $scopesIssued = array(); $scopesNeeded = array(); if ($headers = $e->getHeaders()) { - if ($scopes = $this->remoteFilesystem->findHeaderValue($headers, 'X-OAuth-Scopes')) { + if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-OAuth-Scopes')) { $scopesIssued = explode(' ', $scopes); } - if ($scopes = $this->remoteFilesystem->findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) { + if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) { $scopesNeeded = explode(' ', $scopes); } } @@ -358,7 +364,9 @@ class GitHubDriver extends VcsDriver } if (!$this->io->isInteractive() && $fetchingRepoData) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } $rateLimited = $gitHubUtil->isRateLimited($e->getHeaders()); @@ -404,7 +412,7 @@ class GitHubDriver extends VcsDriver $repoDataUrl = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository; - $this->repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl); + $this->repoData = $this->getContents($repoDataUrl, true)->decodeJson(); if (null === $this->repoData && null !== $this->gitDriver) { return; } @@ -434,7 +442,7 @@ class GitHubDriver extends VcsDriver // are not interactive) then we fallback to GitDriver. $this->setupGitDriver($this->generateSshUrl()); - return; + return true; } catch (\RuntimeException $e) { $this->gitDriver = null; @@ -450,22 +458,19 @@ class GitHubDriver extends VcsDriver $this->io, $this->config, $this->process, - $this->remoteFilesystem + $this->httpDownloader ); $this->gitDriver->initialize(); } - protected function getNextPage() + protected function getNextPage(Response $response) { - $headers = $this->remoteFilesystem->getLastHeaders(); - foreach ($headers as $header) { - if (preg_match('{^link:\s*(.+?)\s*$}i', $header, $match)) { - $links = explode(',', $match[1]); - foreach ($links as $link) { - if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { - return $match[1]; - } - } + $header = $response->getHeader('link'); + + $links = explode(',', $header); + foreach ($links as $link) { + if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { + return $match[1]; } } } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 2044ff702..6a1aa8ac2 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -17,8 +17,9 @@ use Composer\Cache; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Downloader\TransportException; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\GitLab; +use Composer\Util\Http\Response; /** * Driver for GitLab API, use the Git driver for local checkouts. @@ -110,14 +111,14 @@ class GitLabDriver extends VcsDriver } /** - * Updates the RemoteFilesystem instance. + * Updates the HttpDownloader instance. * Mainly useful for tests. * * @internal */ - public function setRemoteFilesystem(RemoteFilesystem $remoteFilesystem) + public function setHttpDownloader(HttpDownloader $httpDownloader) { - $this->remoteFilesystem = $remoteFilesystem; + $this->httpDownloader = $httpDownloader; } /** @@ -140,7 +141,7 @@ class GitLabDriver extends VcsDriver $resource = $this->getApiUrl().'/repository/files/'.$this->urlEncodeAll($file).'/raw?ref='.$identifier; try { - $content = $this->getContents($resource); + $content = $this->getContents($resource)->getBody(); } catch (TransportException $e) { if ($e->getCode() !== 404) { throw $e; @@ -297,7 +298,8 @@ class GitLabDriver extends VcsDriver $references = array(); do { - $data = JsonFile::parseJson($this->getContents($resource), $resource); + $response = $this->getContents($resource); + $data = $response->decodeJson(); foreach ($data as $datum) { $references[$datum['name']] = $datum['commit']['id']; @@ -308,7 +310,7 @@ class GitLabDriver extends VcsDriver } if (count($data) >= $perPage) { - $resource = $this->getNextPage(); + $resource = $this->getNextPage($response); } else { $resource = false; } @@ -321,7 +323,7 @@ class GitLabDriver extends VcsDriver { // we need to fetch the default branch from the api $resource = $this->getApiUrl(); - $this->project = JsonFile::parseJson($this->getContents($resource, true), $resource); + $this->project = $this->getContents($resource, true)->decodeJson(); if (isset($this->project['visibility'])) { $this->isPrivate = $this->project['visibility'] !== 'public'; } else { @@ -344,7 +346,7 @@ class GitLabDriver extends VcsDriver // are not interactive) then we fallback to GitDriver. $this->setupGitDriver($url); - return; + return true; } catch (\RuntimeException $e) { $this->gitDriver = null; @@ -375,7 +377,7 @@ class GitLabDriver extends VcsDriver $this->io, $this->config, $this->process, - $this->remoteFilesystem + $this->httpDownloader ); $this->gitDriver->initialize(); } @@ -386,10 +388,10 @@ class GitLabDriver extends VcsDriver protected function getContents($url, $fetchingRepoData = false) { try { - $res = parent::getContents($url); + $response = parent::getContents($url); if ($fetchingRepoData) { - $json = JsonFile::parseJson($res, $url); + $json = $response->decodeJson(); // force auth as the unauthenticated version of the API is broken if (!isset($json['default_branch'])) { @@ -401,9 +403,9 @@ class GitLabDriver extends VcsDriver } } - return $res; + return $response; } catch (TransportException $e) { - $gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->remoteFilesystem); + $gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->httpDownloader); switch ($e->getCode()) { case 401: @@ -418,7 +420,9 @@ class GitLabDriver extends VcsDriver } if (!$this->io->isInteractive()) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } $this->io->writeError('Failed to download ' . $this->namespace . '/' . $this->repository . ':' . $e->getMessage() . ''); $gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, 'Your credentials are required to fetch private repository metadata ('.$this->url.')'); @@ -431,7 +435,9 @@ class GitLabDriver extends VcsDriver } if (!$this->io->isInteractive() && $fetchingRepoData) { - return $this->attemptCloneFallback(); + if ($this->attemptCloneFallback()) { + return new Response(array('url' => 'dummy'), 200, array(), 'null'); + } } throw $e; @@ -471,17 +477,14 @@ class GitLabDriver extends VcsDriver return true; } - private function getNextPage() + protected function getNextPage(Response $response) { - $headers = $this->remoteFilesystem->getLastHeaders(); - foreach ($headers as $header) { - if (preg_match('{^link:\s*(.+?)\s*$}i', $header, $match)) { - $links = explode(',', $match[1]); - foreach ($links as $link) { - if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { - return $match[1]; - } - } + $header = $response->getHeader('link'); + + $links = explode(',', $header); + foreach ($links as $link) { + if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { + return $match[1]; } } } diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php index 8324f22ac..4a00f2da0 100644 --- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -76,7 +76,7 @@ class HgBitbucketDriver extends BitbucketDriver $this->io, $this->config, $this->process, - $this->remoteFilesystem + $this->httpDownloader ); $this->fallbackDriver->initialize(); } diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index 5227630f6..17ed706d2 100644 --- a/src/Composer/Repository/Vcs/VcsDriver.php +++ b/src/Composer/Repository/Vcs/VcsDriver.php @@ -19,8 +19,9 @@ use Composer\Factory; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; /** * A driver implementation for driver with authentication interaction. @@ -41,8 +42,8 @@ abstract class VcsDriver implements VcsDriverInterface protected $config; /** @var ProcessExecutor */ protected $process; - /** @var RemoteFilesystem */ - protected $remoteFilesystem; + /** @var HttpDownloader */ + protected $httpDownloader; /** @var array */ protected $infoCache = array(); /** @var Cache */ @@ -55,9 +56,9 @@ abstract class VcsDriver implements VcsDriverInterface * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking - * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking */ - final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) + final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null) { if (Filesystem::isLocalPath($repoConfig['url'])) { $repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']); @@ -69,7 +70,7 @@ abstract class VcsDriver implements VcsDriverInterface $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); } /** @@ -156,13 +157,13 @@ abstract class VcsDriver implements VcsDriverInterface * * @param string $url The URL of content * - * @return mixed The result + * @return Response */ protected function getContents($url) { $options = isset($this->repoConfig['options']) ? $this->repoConfig['options'] : array(); - return $this->remoteFilesystem->getContents($this->originUrl, $url, false, $options); + return $this->httpDownloader->get($url, $options); } /** diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 1fc286ac4..d9f569b1b 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -25,7 +25,7 @@ class Bitbucket private $io; private $config; private $process; - private $remoteFilesystem; + private $httpDownloader; private $token = array(); private $time; @@ -37,15 +37,15 @@ class Bitbucket * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking - * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking * @param int $time Timestamp, injectable for mocking */ - public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null, $time = null) + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null, $time = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); $this->time = $time; } @@ -90,7 +90,7 @@ class Bitbucket private function requestAccessToken($originUrl) { try { - $json = $this->remoteFilesystem->getContents($originUrl, self::OAUTH2_ACCESS_TOKEN_URL, false, array( + $response = $this->httpDownloader->get(self::OAUTH2_ACCESS_TOKEN_URL, array( 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', @@ -98,7 +98,7 @@ class Bitbucket ), )); - $this->token = json_decode($json, true); + $this->token = $response->decodeJson(); } catch (TransportException $e) { if ($e->getCode() === 400) { $this->io->writeError('Invalid OAuth consumer provided.'); diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 1eca1a9bb..c3046cb77 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -25,7 +25,7 @@ class GitHub protected $io; protected $config; protected $process; - protected $remoteFilesystem; + protected $httpDownloader; /** * Constructor. @@ -33,14 +33,14 @@ class GitHub * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking - * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking */ - public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); } /** @@ -104,7 +104,7 @@ class GitHub try { $apiUrl = ('github.com' === $originUrl) ? 'api.github.com/' : $originUrl . '/api/v3/'; - $this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl, false, array( + $this->httpDownloader->get('https://'. $apiUrl, array( 'retry-auth-failure' => false, )); } catch (TransportException $e) { diff --git a/src/Composer/Util/GitLab.php b/src/Composer/Util/GitLab.php index 475c5e7ee..2a4867954 100644 --- a/src/Composer/Util/GitLab.php +++ b/src/Composer/Util/GitLab.php @@ -26,7 +26,7 @@ class GitLab protected $io; protected $config; protected $process; - protected $remoteFilesystem; + protected $httpDownloader; /** * Constructor. @@ -34,14 +34,14 @@ class GitLab * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking - * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking */ - public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); + $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); } /** @@ -154,10 +154,10 @@ class GitLab ), ); - $json = $this->remoteFilesystem->getContents($originUrl, $scheme.'://'.$apiUrl.'/oauth/token', false, $options); + $token = $this->httpDownloader->get($scheme.'://'.$apiUrl.'/oauth/token', $options)->decodeJson(); $this->io->writeError('Token successfully created'); - return JsonFile::parseJson($json); + return $token; } } diff --git a/src/Composer/Util/Http/Response.php b/src/Composer/Util/Http/Response.php index ff48fdb40..f76057f3e 100644 --- a/src/Composer/Util/Http/Response.php +++ b/src/Composer/Util/Http/Response.php @@ -23,6 +23,9 @@ class Response public function __construct(array $request, $code, array $headers, $body) { + if (!isset($request['url'])) { + throw new \LogicException('url key missing from request array'); + } $this->request = $request; $this->code = $code; $this->headers = $headers; diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 31c615e0c..631c0df7d 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -117,8 +117,8 @@ class HttpDownloader $origin = $this->getOrigin($job['request']['url']); - // TODO only send http/https through curl - if ($curl) { + // TODO experiment with allowing file:// through curl too + if ($curl && preg_match('{^https?://}i', $job['request']['url'])) { $resolver = function ($resolve, $reject) use (&$job, $curl, $origin) { // start job $url = $job['request']['url']; @@ -141,11 +141,7 @@ class HttpDownloader $job['status'] = HttpDownloader::STATUS_STARTED; if ($job['request']['copyTo']) { - if ($curl) { - $result = $curl->download($origin, $url, $options, $job['request']['copyTo']); - } else { - $result = $rfs->copy($origin, $url, $job['request']['copyTo'], false /* TODO progress */, $options); - } + $result = $rfs->copy($origin, $url, $job['request']['copyTo'], false /* TODO progress */, $options); $resolve($result); } else { diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index f4a211acb..00fe35294 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -147,7 +147,7 @@ class RemoteFilesystem * @param string $name header name (case insensitive) * @return string|null */ - public function findHeaderValue(array $headers, $name) + public static function findHeaderValue(array $headers, $name) { $value = null; foreach ($headers as $header) { @@ -167,7 +167,7 @@ class RemoteFilesystem * @param array $headers array of returned headers like from getLastHeaders() * @return int|null */ - public function findStatusCode(array $headers) + public static function findStatusCode(array $headers) { $value = null; foreach ($headers as $header) { diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php index 68852d8e0..ddf21c64b 100644 --- a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -156,7 +156,11 @@ class ArchiveDownloaderTest extends TestCase { return $this->getMockForAbstractClass( 'Composer\Downloader\ArchiveDownloader', - array($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getMockBuilder('Composer\Config')->getMock()) + array( + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), + $config = $this->getMockBuilder('Composer\Config')->getMock(), + new \Composer\Util\HttpDownloader($io, $config), + ) ); } } diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index 476b9a8f7..12edfe19d 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -18,13 +18,13 @@ use Composer\Util\Filesystem; class FileDownloaderTest extends TestCase { - protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $rfs = null, $filesystem = null) + protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $httpDownloader = null, $filesystem = null) { $io = $io ?: $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $config = $config ?: $this->getMockBuilder('Composer\Config')->getMock(); - $rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock(); + $httpDownloader = $httpDownloader ?: $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); - return new FileDownloader($io, $config, $eventDispatcher, $cache, $rfs, $filesystem); + return new FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $filesystem); } /** diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index 6df782ddb..451592d37 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -16,7 +16,7 @@ use Composer\Downloader\XzDownloader; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Composer\Util\Platform; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; class XzDownloaderTest extends TestCase { @@ -66,7 +66,7 @@ class XzDownloaderTest extends TestCase ->method('get') ->with('vendor-dir') ->will($this->returnValue($this->testDir)); - $downloader = new XzDownloader($io, $config, null, null, null, new RemoteFilesystem($io)); + $downloader = new XzDownloader($io, $config, new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null); try { $downloader->download($packageMock, $this->getUniqueTmpDirectory()); diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 466fd35c7..0c1311427 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -16,6 +16,7 @@ use Composer\Downloader\ZipDownloader; use Composer\Package\PackageInterface; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\HttpDownloader; class ZipDownloaderTest extends TestCase { @@ -32,6 +33,8 @@ class ZipDownloaderTest extends TestCase $this->testDir = $this->getUniqueTmpDirectory(); $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $this->config = $this->getMockBuilder('Composer\Config')->getMock(); + $dlConfig = $this->getMockBuilder('Composer\Config')->getMock(); + $this->httpDownloader = new HttpDownloader($this->io, $dlConfig); } public function tearDown() @@ -64,18 +67,6 @@ class ZipDownloaderTest extends TestCase } $this->config->expects($this->at(0)) - ->method('get') - ->with('disable-tls') - ->will($this->returnValue(false)); - $this->config->expects($this->at(1)) - ->method('get') - ->with('cafile') - ->will($this->returnValue(null)); - $this->config->expects($this->at(2)) - ->method('get') - ->with('capath') - ->will($this->returnValue(null)); - $this->config->expects($this->at(3)) ->method('get') ->with('vendor-dir') ->will($this->returnValue($this->testDir)); @@ -94,7 +85,7 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(array())) ; - $downloader = new ZipDownloader($this->io, $this->config); + $downloader = new ZipDownloader($this->io, $this->config, $this->httpDownloader); $this->setPrivateProperty('hasSystemUnzip', false); @@ -118,8 +109,7 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasZipArchive', true); - $downloader = new MockedZipDownloader($this->io, $this->config); - + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive->expects($this->at(0)) ->method('open') @@ -144,8 +134,7 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasZipArchive', true); - $downloader = new MockedZipDownloader($this->io, $this->config); - + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive->expects($this->at(0)) ->method('open') @@ -169,8 +158,7 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasZipArchive', true); - $downloader = new MockedZipDownloader($this->io, $this->config); - + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive->expects($this->at(0)) ->method('open') @@ -200,7 +188,7 @@ class ZipDownloaderTest extends TestCase ->method('execute') ->will($this->returnValue(1)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $downloader->extract('testfile.zip', 'vendor/dir'); } @@ -217,7 +205,7 @@ class ZipDownloaderTest extends TestCase ->method('execute') ->will($this->returnValue(0)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $downloader->extract('testfile.zip', 'vendor/dir'); } @@ -244,7 +232,7 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(true)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $downloader->extract('testfile.zip', 'vendor/dir'); } @@ -276,7 +264,7 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $downloader->extract('testfile.zip', 'vendor/dir'); } @@ -304,7 +292,7 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $downloader->extract('testfile.zip', 'vendor/dir'); } @@ -336,7 +324,7 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $downloader->extract('testfile.zip', 'vendor/dir'); } diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index f21007281..2e7d70639 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -63,7 +63,9 @@ class InstallerTest extends TestCase ->getMock(); $config = $this->getMockBuilder('Composer\Config')->getMock(); - $repositoryManager = new RepositoryManager($io, $config); + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); + $repositoryManager = new RepositoryManager($io, $config, $eventDispatcher, $httpDownloader); $repositoryManager->setLocalRepository(new InstalledArrayRepository()); if (!is_array($repositories)) { @@ -76,7 +78,6 @@ class InstallerTest extends TestCase $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock(); $installationManager = new InstallationManagerMock(); - $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator); diff --git a/tests/Composer/Test/Mock/RemoteFilesystemMock.php b/tests/Composer/Test/Mock/HttpDownloaderMock.php similarity index 73% rename from tests/Composer/Test/Mock/RemoteFilesystemMock.php rename to tests/Composer/Test/Mock/HttpDownloaderMock.php index 5d4f52e54..1e2774af0 100644 --- a/tests/Composer/Test/Mock/RemoteFilesystemMock.php +++ b/tests/Composer/Test/Mock/HttpDownloaderMock.php @@ -12,13 +12,11 @@ namespace Composer\Test\Mock; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; +use Composer\Util\Http\Response; use Composer\Downloader\TransportException; -/** - * Remote filesystem mock - */ -class RemoteFilesystemMock extends RemoteFilesystem +class HttpDownloaderMock extends HttpDownloader { protected $contentMap; @@ -30,10 +28,10 @@ class RemoteFilesystemMock extends RemoteFilesystem $this->contentMap = $contentMap; } - public function getContents($originUrl, $fileUrl, $progress = true, $options = array()) + public function get($fileUrl, $options = array()) { if (!empty($this->contentMap[$fileUrl])) { - return $this->contentMap[$fileUrl]; + return new Response(array('url' => $fileUrl), 200, array(), $this->contentMap[$fileUrl]); } throw new TransportException('The "'.$fileUrl.'" file could not be downloaded (NOT FOUND)', 404); diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index f9fe308fa..b9f08e693 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -12,9 +12,11 @@ namespace Composer\Test\Package\Archiver; +use Composer\IO\NullIO; use Composer\Factory; use Composer\Package\Archiver\ArchiveManager; use Composer\Package\PackageInterface; +use Composer\Test\Mock\FactoryMock; class ArchiveManagerTest extends ArchiverTest { @@ -30,7 +32,12 @@ class ArchiveManagerTest extends ArchiverTest parent::setUp(); $factory = new Factory(); - $this->manager = $factory->createArchiveManager($factory->createConfig()); + $dm = $factory->createDownloadManager( + $io = new NullIO, + $config = FactoryMock::createConfig(), + $factory->createHttpDownloader($io, $config) + ); + $this->manager = $factory->createArchiveManager($factory->createConfig(), $dm); $this->targetDir = $this->testDir.'/composer_archiver_tests'; } diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 05e1afada..1fe60f589 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -18,7 +18,7 @@ use Composer\Repository\RepositoryInterface; use Composer\Test\Mock\FactoryMock; use Composer\Test\TestCase; use Composer\Package\Loader\ArrayLoader; -use Composer\Semver\VersionParser; +use Composer\Package\Version\VersionParser; class ComposerRepositoryTest extends TestCase { @@ -37,6 +37,8 @@ class ComposerRepositoryTest extends TestCase $repoConfig, new NullIO, FactoryMock::createConfig(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock() )) ->getMock(); @@ -179,21 +181,29 @@ class ComposerRepositoryTest extends TestCase ), ); - $rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->disableOriginalConstructor() + ->getMock(); + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->disableOriginalConstructor() ->getMock(); - $rfs->expects($this->at(0)) - ->method('getContents') - ->with('example.org', 'http://example.org/packages.json', false) - ->willReturn(json_encode(array('search' => '/search.json?q=%query%&type=%type%'))); + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($url = 'http://example.org/packages.json') + ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array('search' => '/search.json?q=%query%&type=%type%')))); - $rfs->expects($this->at(1)) - ->method('getContents') - ->with('example.org', 'http://example.org/search.json?q=foo&type=composer-plugin', false) - ->willReturn(json_encode($result)); + $httpDownloader->expects($this->at(1)) + ->method('get') + ->with($url = 'http://example.org/search.json?q=foo&type=composer-plugin') + ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode($result))); - $repository = new ComposerRepository($repoConfig, new NullIO, FactoryMock::createConfig(), null, $rfs); + $httpDownloader->expects($this->at(2)) + ->method('get') + ->with($url = 'http://example.org/search.json?q=foo&type=library') + ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array()))); + + $repository = new ComposerRepository($repoConfig, new NullIO, FactoryMock::createConfig(), $eventDispatcher, $httpDownloader); $this->assertSame( array(array('name' => 'foo', 'description' => null)), diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php index a9594257c..abe6063f4 100644 --- a/tests/Composer/Test/Repository/PathRepositoryTest.php +++ b/tests/Composer/Test/Repository/PathRepositoryTest.php @@ -14,8 +14,8 @@ namespace Composer\Test\Repository; use Composer\Package\Loader\ArrayLoader; use Composer\Repository\PathRepository; -use Composer\Semver\VersionParser; use Composer\Test\TestCase; +use Composer\Package\Version\VersionParser; class PathRepositoryTest extends TestCase { diff --git a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php index e766065a7..7ad30825d 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php @@ -22,13 +22,13 @@ use Composer\Semver\VersionParser; use Composer\Semver\Constraint\Constraint; use Composer\Package\Link; use Composer\Package\CompletePackage; -use Composer\Test\Mock\RemoteFilesystemMock; +use Composer\Test\Mock\HttpDownloaderMock; class ChannelReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new RemoteFilesystemMock(array( + $rfs = new HttpDownloaderMock(array( 'http://pear.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'), 'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'), 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), @@ -50,11 +50,15 @@ class ChannelReaderTest extends TestCase public function testShouldSelectCorrectReader() { - $rfs = new RemoteFilesystemMock(array( + $rfs = new HttpDownloaderMock(array( 'http://pear.1.0.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.0.xml'), 'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'), 'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'), + 'http://test.loc/rest10/r/http_client/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_allreleases.xml'), + 'http://test.loc/rest10/r/http_client/deps.1.2.1.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_deps.1.2.1.txt'), 'http://test.loc/rest10/p/http_request/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_info.xml'), + 'http://test.loc/rest10/r/http_request/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_allreleases.xml'), + 'http://test.loc/rest10/r/http_request/deps.1.4.0.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_deps.1.4.0.txt'), 'http://pear.1.1.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'), 'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'), 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), diff --git a/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php index 4aa7bbba2..5a40915e1 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php @@ -13,13 +13,13 @@ namespace Composer\Test\Repository\Pear; use Composer\Test\TestCase; -use Composer\Test\Mock\RemoteFilesystemMock; +use Composer\Test\Mock\HttpDownloaderMock; class ChannelRest10ReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new RemoteFilesystemMock(array( + $rfs = new HttpDownloaderMock(array( 'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'), 'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'), 'http://test.loc/rest10/r/http_client/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_allreleases.xml'), diff --git a/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php index 04e48426e..08c3a2998 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php @@ -13,13 +13,13 @@ namespace Composer\Test\Repository\Pear; use Composer\Test\TestCase; -use Composer\Test\Mock\RemoteFilesystemMock; +use Composer\Test\Mock\HttpDownloaderMock; class ChannelRest11ReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new RemoteFilesystemMock(array( + $rfs = new HttpDownloaderMock(array( 'http://pear.1.1.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'), 'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'), 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), diff --git a/tests/Composer/Test/Repository/PearRepositoryTest.php b/tests/Composer/Test/Repository/PearRepositoryTest.php index b1a3c0b5e..6046fefb4 100644 --- a/tests/Composer/Test/Repository/PearRepositoryTest.php +++ b/tests/Composer/Test/Repository/PearRepositoryTest.php @@ -133,7 +133,7 @@ class PearRepositoryTest extends TestCase $config = new \Composer\Config(); - $this->remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + $this->httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock(); @@ -143,6 +143,6 @@ class PearRepositoryTest extends TestCase protected function tearDown() { $this->repository = null; - $this->remoteFilesystem = null; + $this->httpDownloader = null; } } diff --git a/tests/Composer/Test/Repository/RepositoryFactoryTest.php b/tests/Composer/Test/Repository/RepositoryFactoryTest.php index e54624415..e0a854d46 100644 --- a/tests/Composer/Test/Repository/RepositoryFactoryTest.php +++ b/tests/Composer/Test/Repository/RepositoryFactoryTest.php @@ -21,7 +21,9 @@ class RepositoryFactoryTest extends TestCase { $manager = RepositoryFactory::manager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), - $this->getMockBuilder('Composer\Config')->getMock() + $this->getMockBuilder('Composer\Config')->getMock(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); $ref = new \ReflectionProperty($manager, 'repositoryClasses'); diff --git a/tests/Composer/Test/Repository/RepositoryManagerTest.php b/tests/Composer/Test/Repository/RepositoryManagerTest.php index 3774dd268..c4f09de87 100644 --- a/tests/Composer/Test/Repository/RepositoryManagerTest.php +++ b/tests/Composer/Test/Repository/RepositoryManagerTest.php @@ -38,7 +38,8 @@ class RepositoryManagerTest extends TestCase $rm = new RepositoryManager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getMockBuilder('Composer\Config')->getMock(), - $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock() ); $repository1 = $this->getMockBuilder('Composer\Repository\RepositoryInterface')->getMock(); @@ -61,7 +62,8 @@ class RepositoryManagerTest extends TestCase $rm = new RepositoryManager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $config = $this->getMockBuilder('Composer\Config')->setMethods(array('get'))->getMock(), - $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock() ); $tmpdir = $this->tmpdir; diff --git a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php index 8d711e8f0..a3a9219d9 100644 --- a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php @@ -16,6 +16,7 @@ use Composer\Config; use Composer\Repository\Vcs\GitBitbucketDriver; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; /** * @group bitbucket @@ -26,8 +27,8 @@ class GitBitbucketDriverTest extends TestCase private $io; /** @type \Composer\Config */ private $config; - /** @type \Composer\Util\RemoteFilesystem|\PHPUnit_Framework_MockObject_MockObject */ - private $rfs; + /** @type \Composer\Util\HttpDownloader|\PHPUnit_Framework_MockObject_MockObject */ + private $httpDownloader; /** @type string */ private $home; /** @type string */ @@ -46,7 +47,7 @@ class GitBitbucketDriverTest extends TestCase ), )); - $this->rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + $this->httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock(); } @@ -68,7 +69,7 @@ class GitBitbucketDriverTest extends TestCase $this->io, $this->config, null, - $this->rfs + $this->httpDownloader ); $driver->initialize(); @@ -83,15 +84,14 @@ class GitBitbucketDriverTest extends TestCase 'https://bitbucket.org/user/repo.git does not appear to be a git repository, use https://bitbucket.org/user/repo if this is a mercurial bitbucket repository' ); - $this->rfs->expects($this->once()) - ->method('getContents') + $this->httpDownloader->expects($this->once()) + ->method('get') ->with( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner', - false + $url = 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner', + array() ) ->willReturn( - '{"scm":"hg","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo","name":"https"},{"href":"ssh:\/\/hg@bitbucket.org\/user\/repo","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}' + new Response(array('url' => $url), 200, array(), '{"scm":"hg","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo","name":"https"},{"href":"ssh:\/\/hg@bitbucket.org\/user\/repo","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}') ); $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); @@ -103,47 +103,43 @@ class GitBitbucketDriverTest extends TestCase { $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); - $this->rfs->expects($this->any()) - ->method('getContents') + $urls = array( + 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner', + 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=mainbranch', + 'https://api.bitbucket.org/2.0/repositories/user/repo/refs/tags?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cnext&sort=-target.date', + 'https://api.bitbucket.org/2.0/repositories/user/repo/refs/branches?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cvalues.heads%2Cnext&sort=-target.date', + 'https://api.bitbucket.org/2.0/repositories/user/repo/src/master/composer.json', + 'https://api.bitbucket.org/2.0/repositories/user/repo/commit/master?fields=date', + ); + $this->httpDownloader->expects($this->any()) + ->method('get') ->withConsecutive( array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner', - false, + $urls[0], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo?fields=mainbranch', - false, + $urls[1], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo/refs/tags?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cnext&sort=-target.date', - false, + $urls[2], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo/refs/branches?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cvalues.heads%2Cnext&sort=-target.date', - false, + $urls[3], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo/src/master/composer.json', - false, + $urls[4], array() ), array( - $this->originUrl, - 'https://api.bitbucket.org/2.0/repositories/user/repo/commit/master?fields=date', - false, + $urls[5], array() ) ) ->willReturnOnConsecutiveCalls( - '{"scm":"git","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo.git","name":"https"},{"href":"ssh:\/\/git@bitbucket.org\/user\/repo.git","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}', - '{"mainbranch": {"name": "master"}}', - '{"values":[{"name":"1.0.1","target":{"hash":"9b78a3932143497c519e49b8241083838c8ff8a1"}},{"name":"1.0.0","target":{"hash":"d3393d514318a9267d2f8ebbf463a9aaa389f8eb"}}]}', - '{"values":[{"name":"master","target":{"hash":"937992d19d72b5116c3e8c4a04f960e5fa270b22"}}]}', - '{"name": "user/repo","description": "test repo","license": "GPL","authors": [{"name": "Name","email": "local@domain.tld"}],"require": {"creator/package": "^1.0"},"require-dev": {"phpunit/phpunit": "~4.8"}}', - '{"date": "2016-05-17T13:19:52+00:00"}' + new Response(array('url' => $urls[0]), 200, array(), '{"scm":"git","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo.git","name":"https"},{"href":"ssh:\/\/git@bitbucket.org\/user\/repo.git","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}'), + new Response(array('url' => $urls[1]), 200, array(), '{"mainbranch": {"name": "master"}}'), + new Response(array('url' => $urls[2]), 200, array(), '{"values":[{"name":"1.0.1","target":{"hash":"9b78a3932143497c519e49b8241083838c8ff8a1"}},{"name":"1.0.0","target":{"hash":"d3393d514318a9267d2f8ebbf463a9aaa389f8eb"}}]}'), + new Response(array('url' => $urls[3]), 200, array(), '{"values":[{"name":"master","target":{"hash":"937992d19d72b5116c3e8c4a04f960e5fa270b22"}}]}'), + new Response(array('url' => $urls[4]), 200, array(), '{"name": "user/repo","description": "test repo","license": "GPL","authors": [{"name": "Name","email": "local@domain.tld"}],"require": {"creator/package": "^1.0"},"require-dev": {"phpunit/phpunit": "~4.8"}}'), + new Response(array('url' => $urls[5]), 200, array(), '{"date": "2016-05-17T13:19:52+00:00"}') ); $this->assertEquals( diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index ba9c6d4f7..1f721a4c7 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -16,6 +16,7 @@ use Composer\Downloader\TransportException; use Composer\Repository\Vcs\GitHubDriver; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; use Composer\Config; class GitHubDriverTest extends TestCase @@ -53,8 +54,8 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(true)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) ->getMock(); $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); @@ -62,9 +63,9 @@ class GitHubDriverTest extends TestCase ->method('execute') ->will($this->returnValue(1)); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($repoApiUrl)) ->will($this->throwException(new TransportException('HTTP/1.1 404 Not Found', 404))); $io->expects($this->once()) @@ -76,15 +77,15 @@ class GitHubDriverTest extends TestCase ->method('setAuthentication') ->with($this->equalTo('github.com'), $this->matchesRegularExpression('{sometoken}'), $this->matchesRegularExpression('{x-oauth-basic}')); - $remoteFilesystem->expects($this->at(1)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/'), $this->equalTo(false)) - ->will($this->returnValue('{}')); + $httpDownloader->expects($this->at(1)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/')) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{}'))); - $remoteFilesystem->expects($this->at(2)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "private": true, "owner": {"login": "composer"}, "name": "packagist"}')); + $httpDownloader->expects($this->at(2)) + ->method('get') + ->with($this->equalTo($url = $repoApiUrl)) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"master_branch": "test_master", "private": true, "owner": {"login": "composer"}, "name": "packagist"}'))); $configSource = $this->getMockBuilder('Composer\Config\ConfigSourceInterface')->getMock(); $authConfigSource = $this->getMockBuilder('Composer\Config\ConfigSourceInterface')->getMock(); @@ -95,7 +96,7 @@ class GitHubDriverTest extends TestCase 'url' => $repoUrl, ); - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $process, $remoteFilesystem); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $process, $httpDownloader); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -124,21 +125,21 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(true)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) ->getMock(); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}')); + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($repoApiUrl)) + ->will($this->returnValue(new Response(array('url' => $repoApiUrl), 200, array(), '{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}'))); $repoConfig = array( 'url' => $repoUrl, ); $repoUrl = 'https://github.com/composer/packagist.git'; - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $remoteFilesystem); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $httpDownloader); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -167,31 +168,31 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(true)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) ->getMock(); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}')); + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($url = $repoApiUrl)) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}'))); - $remoteFilesystem->expects($this->at(1)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo'), $this->equalTo(false)) - ->will($this->returnValue('{"encoding":"base64","content":"'.base64_encode('{"support": {"source": "'.$repoUrl.'" }}').'"}')); + $httpDownloader->expects($this->at(1)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo')) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"encoding":"base64","content":"'.base64_encode('{"support": {"source": "'.$repoUrl.'" }}').'"}'))); - $remoteFilesystem->expects($this->at(2)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo'), $this->equalTo(false)) - ->will($this->returnValue('{"commit": {"committer":{ "date": "2012-09-10"}}}')); + $httpDownloader->expects($this->at(2)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo')) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"commit": {"committer":{ "date": "2012-09-10"}}}'))); $repoConfig = array( 'url' => $repoUrl, ); $repoUrl = 'https://github.com/composer/packagist.git'; - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $remoteFilesystem); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $httpDownloader); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -227,13 +228,13 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(false)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) ->getMock(); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($repoApiUrl)) ->will($this->throwException(new TransportException('HTTP/1.1 404 Not Found', 404))); // clean local clone if present @@ -278,7 +279,7 @@ class GitHubDriverTest extends TestCase 'url' => $repoUrl, ); - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $process, $remoteFilesystem); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $process, $httpDownloader); $gitHubDriver->initialize(); $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); diff --git a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php index a5eb799f2..f940733ae 100644 --- a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Prophecy\Argument; +use Composer\Util\Http\Response; /** * @author Jérôme Tamarelle @@ -27,7 +28,7 @@ class GitLabDriverTest extends TestCase private $config; private $io; private $process; - private $remoteFilesystem; + private $httpDownloader; public function setUp() { @@ -47,7 +48,7 @@ class GitLabDriverTest extends TestCase $this->io = $this->prophesize('Composer\IO\IOInterface'); $this->process = $this->prophesize('Composer\Util\ProcessExecutor'); - $this->remoteFilesystem = $this->prophesize('Composer\Util\RemoteFilesystem'); + $this->httpDownloader = $this->prophesize('Composer\Util\HttpDownloader'); } public function tearDown() @@ -87,13 +88,11 @@ class GitLabDriverTest extends TestCase } JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -126,13 +125,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -164,13 +161,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -206,12 +201,10 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents($domain, $apiUrl, false, array()) - ->willReturn(sprintf($projectData, $domain, $port, $namespace)) + $this->mockResponse($apiUrl, array(), sprintf($projectData, $domain, $port, $namespace)) ->shouldBeCalledTimes(1); - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -289,15 +282,11 @@ JSON; ] JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($tagData) + $this->mockResponse($apiUrl, array(), $tagData) ->shouldBeCalledTimes(1) ; - $this->remoteFilesystem->getLastHeaders() - ->willReturn(array()); - $driver->setRemoteFilesystem($this->remoteFilesystem->reveal()); + $driver->setHttpDownloader($this->httpDownloader->reveal()); $expected = array( 'v1.0.0' => '092ed2c762bbae331e3f51d4a17f67310bf99a81', @@ -344,26 +333,20 @@ JSON; $branchData = json_encode($branchData); - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($branchData) - ->shouldBeCalledTimes(1) - ; + $headers = array('Link: ; rel="next", ; rel="first", ; rel="last"'); + $this->httpDownloader + ->get($apiUrl, array()) + ->willReturn(new Response(array('url' => $apiUrl), 200, $headers, $branchData)) + ->shouldBeCalledTimes(1); - $this->remoteFilesystem - ->getContents('gitlab.com', "http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20", false, array()) - ->willReturn($branchData) - ->shouldBeCalledTimes(1) - ; + $apiUrl = "http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20"; + $headers = array('Link: ; rel="prev", ; rel="first", ; rel="last"'); + $this->httpDownloader + ->get($apiUrl, array()) + ->willReturn(new Response(array('url' => $apiUrl), 200, $headers, $branchData)) + ->shouldBeCalledTimes(1); - $this->remoteFilesystem->getLastHeaders() - ->willReturn( - array('Link: ; rel="next", ; rel="first", ; rel="last"'), - array('Link: ; rel="prev", ; rel="first", ; rel="last"') - ) - ->shouldBeCalledTimes(2); - - $driver->setRemoteFilesystem($this->remoteFilesystem->reveal()); + $driver->setHttpDownloader($this->httpDownloader->reveal()); $expected = array( 'mymaster' => '97eda36b5c1dd953a3792865c222d4e85e5f302e', @@ -401,15 +384,11 @@ JSON; ] JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($branchData) + $this->mockResponse($apiUrl, array(), $branchData) ->shouldBeCalledTimes(1) ; - $this->remoteFilesystem->getLastHeaders() - ->willReturn(array()); - $driver->setRemoteFilesystem($this->remoteFilesystem->reveal()); + $driver->setHttpDownloader($this->httpDownloader->reveal()); $expected = array( 'mymaster' => '97eda36b5c1dd953a3792865c222d4e85e5f302e', @@ -474,13 +453,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('mycompany.com/gitlab', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -507,13 +484,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('gitlab.com', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -540,13 +515,11 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents('mycompany.com/gitlab', $apiUrl, false, array()) - ->willReturn($projectData) + $this->mockResponse($apiUrl, array(), $projectData) ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->remoteFilesystem->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -575,9 +548,7 @@ JSON; } JSON; - $this->remoteFilesystem - ->getContents(Argument::cetera(), $options) - ->willReturn($projectData) + $this->mockResponse(Argument::cetera(), $options, $projectData) ->shouldBeCalled(); $driver = new GitLabDriver( @@ -585,8 +556,15 @@ JSON; $this->io->reveal(), $this->config, $this->process->reveal(), - $this->remoteFilesystem->reveal() + $this->httpDownloader->reveal() ); $driver->initialize(); } + + private function mockResponse($url, $options, $return) + { + return $this->httpDownloader + ->get($url, $options) + ->willReturn(new Response(array('url' => $url), 200, array(), $return)); + } } diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php index a5e5d4b4c..8225a4a54 100644 --- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -26,7 +26,7 @@ class PerforceDriverTest extends TestCase protected $config; protected $io; protected $process; - protected $remoteFileSystem; + protected $httpDownloader; protected $testPath; protected $driver; protected $repoConfig; @@ -43,9 +43,9 @@ class PerforceDriverTest extends TestCase $this->repoConfig = $this->getTestRepoConfig(); $this->io = $this->getMockIOInterface(); $this->process = $this->getMockProcessExecutor(); - $this->remoteFileSystem = $this->getMockRemoteFilesystem(); + $this->httpDownloader = $this->getMockHttpDownloader(); $this->perforce = $this->getMockPerforce(); - $this->driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); + $this->driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->httpDownloader); $this->overrideDriverInternalPerforce($this->perforce); } @@ -56,7 +56,7 @@ class PerforceDriverTest extends TestCase $fs->removeDirectory($this->testPath); $this->driver = null; $this->perforce = null; - $this->remoteFileSystem = null; + $this->httpDownloader = null; $this->process = null; $this->io = null; $this->repoConfig = null; @@ -99,9 +99,9 @@ class PerforceDriverTest extends TestCase return $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); } - protected function getMockRemoteFilesystem() + protected function getMockHttpDownloader() { - return $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock(); + return $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); } protected function getMockPerforce() @@ -113,7 +113,7 @@ class PerforceDriverTest extends TestCase public function testInitializeCapturesVariablesFromRepoConfig() { - $driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); + $driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->httpDownloader); $driver->initialize(); $this->assertEquals(self::TEST_URL, $driver->getUrl()); $this->assertEquals(self::TEST_DEPOT, $driver->getDepot()); diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index 4323a15f5..f50bdd818 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Util; use Composer\Util\Bitbucket; +use Composer\Util\Http\Response; use PHPUnit\Framework\TestCase; /** @@ -30,8 +31,8 @@ class BitbucketTest extends TestCase /** @type \Composer\IO\ConsoleIO|\PHPUnit_Framework_MockObject_MockObject */ private $io; - /** @type \Composer\Util\RemoteFilesystem|\PHPUnit_Framework_MockObject_MockObject */ - private $rfs; + /** @type \Composer\Util\HttpDownloader|\PHPUnit_Framework_MockObject_MockObject */ + private $httpDownloader; /** @type \Composer\Config|\PHPUnit_Framework_MockObject_MockObject */ private $config; /** @type Bitbucket */ @@ -47,8 +48,8 @@ class BitbucketTest extends TestCase ->getMock() ; - $this->rfs = $this - ->getMockBuilder('Composer\Util\RemoteFilesystem') + $this->httpDownloader = $this + ->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock() ; @@ -57,7 +58,7 @@ class BitbucketTest extends TestCase $this->time = time(); - $this->bitbucket = new Bitbucket($this->io, $this->config, null, $this->rfs, $this->time); + $this->bitbucket = new Bitbucket($this->io, $this->config, null, $this->httpDownloader, $this->time); } public function testRequestAccessTokenWithValidOAuthConsumer() @@ -66,12 +67,10 @@ class BitbucketTest extends TestCase ->method('setAuthentication') ->with($this->origin, $this->consumer_key, $this->consumer_secret); - $this->rfs->expects($this->once()) - ->method('getContents') + $this->httpDownloader->expects($this->once()) + ->method('get') ->with( - $this->origin, Bitbucket::OAUTH2_ACCESS_TOKEN_URL, - false, array( 'retry-auth-failure' => false, 'http' => array( @@ -81,9 +80,14 @@ class BitbucketTest extends TestCase ) ) ->willReturn( - sprintf( - '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', - $this->token + new Response( + array('url' => Bitbucket::OAUTH2_ACCESS_TOKEN_URL), + 200, + array(), + sprintf( + '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', + $this->token + ) ) ); @@ -142,12 +146,10 @@ class BitbucketTest extends TestCase ->method('setAuthentication') ->with($this->origin, $this->consumer_key, $this->consumer_secret); - $this->rfs->expects($this->once()) - ->method('getContents') + $this->httpDownloader->expects($this->once()) + ->method('get') ->with( - $this->origin, Bitbucket::OAUTH2_ACCESS_TOKEN_URL, - false, array( 'retry-auth-failure' => false, 'http' => array( @@ -157,9 +159,14 @@ class BitbucketTest extends TestCase ) ) ->willReturn( - sprintf( - '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', - $this->token + new Response( + array('url' => Bitbucket::OAUTH2_ACCESS_TOKEN_URL), + 200, + array(), + sprintf( + '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', + $this->token + ) ) ); @@ -186,12 +193,10 @@ class BitbucketTest extends TestCase array('2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url') ); - $this->rfs->expects($this->once()) - ->method('getContents') + $this->httpDownloader->expects($this->once()) + ->method('get') ->with( - $this->origin, Bitbucket::OAUTH2_ACCESS_TOKEN_URL, - false, array( 'retry-auth-failure' => false, 'http' => array( @@ -234,21 +239,24 @@ class BitbucketTest extends TestCase ) ->willReturnOnConsecutiveCalls($this->consumer_key, $this->consumer_secret); - $this->rfs + $this->httpDownloader ->expects($this->once()) - ->method('getContents') + ->method('get') ->with( - $this->equalTo($this->origin), - $this->equalTo(sprintf('https://%s/site/oauth2/access_token', $this->origin)), - $this->isFalse(), + $this->equalTo($url = sprintf('https://%s/site/oauth2/access_token', $this->origin)), $this->anything() ) ->willReturn( - sprintf( - '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refresh_token", "token_type": "bearer"}', - $this->token + new Response( + array('url' => $url), + 200, + array(), + sprintf( + '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refresh_token", "token_type": "bearer"}', + $this->token + ) ) - ) + ); ; $this->setExpectationsForStoringAccessToken(true); diff --git a/tests/Composer/Test/Util/GitHubTest.php b/tests/Composer/Test/Util/GitHubTest.php index 28d00ce69..1893486e0 100644 --- a/tests/Composer/Test/Util/GitHubTest.php +++ b/tests/Composer/Test/Util/GitHubTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Util; use Composer\Downloader\TransportException; use Composer\Util\GitHub; +use Composer\Util\Http\Response; use PHPUnit\Framework\TestCase; use RecursiveArrayIterator; use RecursiveIteratorIterator; @@ -45,17 +46,15 @@ class GitHubTest extends TestCase ->willReturn($this->password) ; - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader ->expects($this->once()) - ->method('getContents') + ->method('get') ->with( - $this->equalTo($this->origin), - $this->equalTo(sprintf('https://api.%s/', $this->origin)), - $this->isFalse(), + $this->equalTo($url = sprintf('https://api.%s/', $this->origin)), $this->anything() ) - ->willReturn('{}') + ->willReturn(new Response(array('url' => $url), 200, array(), '{}')); ; $config = $this->getConfigMock(); @@ -70,7 +69,7 @@ class GitHubTest extends TestCase ->willReturn($this->getConfJsonMock()) ; - $github = new GitHub($io, $config, null, $rfs); + $github = new GitHub($io, $config, null, $httpDownloader); $this->assertTrue($github->authorizeOAuthInteractively($this->origin, $this->message)); } @@ -85,10 +84,10 @@ class GitHubTest extends TestCase ->willReturn($this->password) ; - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader ->expects($this->exactly(1)) - ->method('getContents') + ->method('get') ->will($this->throwException(new TransportException('', 401))) ; @@ -99,7 +98,7 @@ class GitHubTest extends TestCase ->willReturn($this->getAuthJsonMock()) ; - $github = new GitHub($io, $config, null, $rfs); + $github = new GitHub($io, $config, null, $httpDownloader); $this->assertFalse($github->authorizeOAuthInteractively($this->origin)); } @@ -120,15 +119,15 @@ class GitHubTest extends TestCase return $this->getMockBuilder('Composer\Config')->getMock(); } - private function getRemoteFilesystemMock() + private function getHttpDownloaderMock() { - $rfs = $this - ->getMockBuilder('Composer\Util\RemoteFilesystem') + $httpDownloader = $this + ->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock() ; - return $rfs; + return $httpDownloader; } private function getAuthJsonMock() diff --git a/tests/Composer/Test/Util/GitLabTest.php b/tests/Composer/Test/Util/GitLabTest.php index 27f46b4ad..611b25256 100644 --- a/tests/Composer/Test/Util/GitLabTest.php +++ b/tests/Composer/Test/Util/GitLabTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Util; use Composer\Downloader\TransportException; use Composer\Util\GitLab; +use Composer\Util\Http\Response; use PHPUnit\Framework\TestCase; /** @@ -49,17 +50,15 @@ class GitLabTest extends TestCase ->willReturn($this->password) ; - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader ->expects($this->once()) - ->method('getContents') + ->method('get') ->with( - $this->equalTo($this->origin), - $this->equalTo(sprintf('http://%s/oauth/token', $this->origin)), - $this->isFalse(), + $this->equalTo($url = sprintf('http://%s/oauth/token', $this->origin)), $this->anything() ) - ->willReturn(sprintf('{"access_token": "%s", "token_type": "bearer", "expires_in": 7200}', $this->token)) + ->willReturn(new Response(array('url' => $url), 200, array(), sprintf('{"access_token": "%s", "token_type": "bearer", "expires_in": 7200}', $this->token))); ; $config = $this->getConfigMock(); @@ -69,7 +68,7 @@ class GitLabTest extends TestCase ->willReturn($this->getAuthJsonMock()) ; - $gitLab = new GitLab($io, $config, null, $rfs); + $gitLab = new GitLab($io, $config, null, $httpDownloader); $this->assertTrue($gitLab->authorizeOAuthInteractively('http', $this->origin, $this->message)); } @@ -94,10 +93,10 @@ class GitLabTest extends TestCase ->willReturn($this->password) ; - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $httpDownloader = $this->getHttpDownloaderMock(); + $httpDownloader ->expects($this->exactly(5)) - ->method('getContents') + ->method('get') ->will($this->throwException(new TransportException('', 401))) ; @@ -108,7 +107,7 @@ class GitLabTest extends TestCase ->willReturn($this->getAuthJsonMock()) ; - $gitLab = new GitLab($io, $config, null, $rfs); + $gitLab = new GitLab($io, $config, null, $httpDownloader); $gitLab->authorizeOAuthInteractively('https', $this->origin); } @@ -129,15 +128,15 @@ class GitLabTest extends TestCase return $this->getMockBuilder('Composer\Config')->getMock(); } - private function getRemoteFilesystemMock() + private function getHttpDownloaderMock() { - $rfs = $this - ->getMockBuilder('Composer\Util\RemoteFilesystem') + $httpDownloader = $this + ->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock() ; - return $rfs; + return $httpDownloader; } private function getAuthJsonMock() From f946d8eb5a0104d3aefbd5b5fb8803355f482abf Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 12 Nov 2018 15:34:54 +0100 Subject: [PATCH 027/321] More RemoteFilesystem usage removals and some repository/vcs driver refactorings --- doc/articles/plugins.md | 2 +- doc/articles/scripts.md | 2 +- src/Composer/Command/DiagnoseCommand.php | 29 ++++++++-------- src/Composer/Command/SelfUpdateCommand.php | 8 ++--- src/Composer/Compiler.php | 1 + src/Composer/Factory.php | 12 +++---- src/Composer/Json/JsonFile.php | 20 +++++------ .../Package/Version/VersionGuesser.php | 3 +- .../Repository/ComposerRepository.php | 33 ++++++++++++------- src/Composer/Repository/PearRepository.php | 2 +- src/Composer/Repository/RepositoryFactory.php | 8 ++--- src/Composer/Repository/RepositoryManager.php | 8 ++--- .../Repository/Vcs/GitBitbucketDriver.php | 4 +-- src/Composer/Repository/Vcs/GitHubDriver.php | 4 +-- src/Composer/Repository/Vcs/GitLabDriver.php | 4 +-- .../Repository/Vcs/HgBitbucketDriver.php | 4 +-- src/Composer/Repository/Vcs/VcsDriver.php | 8 ++--- src/Composer/Repository/VcsRepository.php | 14 +++++--- src/Composer/SelfUpdate/Versions.php | 10 +++--- src/Composer/Util/HttpDownloader.php | 20 +++++++++++ .../Downloader/PerforceDownloaderTest.php | 3 +- tests/Composer/Test/InstallerTest.php | 2 +- .../Repository/ComposerRepositoryTest.php | 6 ++-- .../Test/Repository/PearRepositoryTest.php | 2 +- .../Test/Repository/RepositoryManagerTest.php | 8 ++--- .../Repository/Vcs/GitBitbucketDriverTest.php | 5 +-- .../Test/Repository/Vcs/GitHubDriverTest.php | 16 ++++++--- .../Test/Repository/Vcs/GitLabDriverTest.php | 18 +++++----- .../Repository/Vcs/PerforceDriverTest.php | 4 +-- .../Test/Repository/Vcs/SvnDriverTest.php | 3 +- 30 files changed, 156 insertions(+), 107 deletions(-) diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index 59e2a2f15..da20193f6 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -176,7 +176,7 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface if ($protocol === 's3') { $awsClient = new AwsClient($this->io, $this->composer->getConfig()); - $s3Downloader = new S3Downloader($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient); + $s3Downloader = new S3Downloader($this->io, $event->getHttpDownloader()->getOptions(), $awsClient); $event->setHttpdownloader($s3Downloader); } } diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index e0c27b10f..17c83c373 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -61,7 +61,7 @@ Composer fires the following named events during its execution process: - **command**: occurs before any Composer Command is executed on the CLI. It provides you with access to the input and output objects of the program. - **pre-file-download**: occurs before files are downloaded and allows - you to manipulate the `RemoteFilesystem` object prior to downloading files + you to manipulate the `HttpDownloader` object prior to downloading files based on the URL to be downloaded. - **pre-command-run**: occurs before a command is executed and allows you to manipulate the `InputInterface` object's options and arguments to tweak diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 3c4c3bb32..481d58060 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -22,7 +22,7 @@ use Composer\Plugin\PluginEvents; use Composer\Util\ConfigValidator; use Composer\Util\IniHelper; use Composer\Util\ProcessExecutor; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Util\StreamContextFactory; use Composer\SelfUpdate\Keys; use Composer\SelfUpdate\Versions; @@ -35,8 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface; */ class DiagnoseCommand extends BaseCommand { - /** @var RemoteFilesystem */ - protected $rfs; + /** @var HttpDownloader */ + protected $httpDownloader; /** @var ProcessExecutor */ protected $process; @@ -85,7 +85,7 @@ EOT $config->merge(array('config' => array('secure-http' => false))); $config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO); - $this->rfs = Factory::createRemoteFilesystem($io, $config); + $this->httpDownloader = Factory::createHttpDownloader($io, $config); $this->process = new ProcessExecutor($io); $io->write('Checking platform settings: ', false); @@ -226,7 +226,7 @@ EOT } try { - $this->rfs->getContents('packagist.org', $proto . '://repo.packagist.org/packages.json', false); + $this->httpDownloader->get($proto . '://repo.packagist.org/packages.json'); } catch (TransportException $e) { if (false !== strpos($e->getMessage(), 'cafile')) { $result[] = '[' . get_class($e) . '] ' . $e->getMessage() . ''; @@ -253,11 +253,11 @@ EOT $protocol = extension_loaded('openssl') ? 'https' : 'http'; try { - $json = json_decode($this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/packages.json', false), true); + $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->parseJson(); $hash = reset($json['provider-includes']); $hash = $hash['sha256']; $path = str_replace('%hash%', $hash, key($json['provider-includes'])); - $provider = $this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/'.$path, false); + $provider = $this->httpDownloader->get($protocol . '://repo.packagist.org/'.$path)->getBody(); if (hash('sha256', $provider) !== $hash) { return 'It seems that your proxy is modifying http traffic on the fly'; @@ -285,10 +285,10 @@ EOT $url = 'http://repo.packagist.org/packages.json'; try { - $this->rfs->getContents('packagist.org', $url, false); + $this->httpDownloader->get($url); } catch (TransportException $e) { try { - $this->rfs->getContents('packagist.org', $url, false, array('http' => array('request_fulluri' => false))); + $this->httpDownloader->get($url, array('http' => array('request_fulluri' => false))); } catch (TransportException $e) { return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')'; } @@ -319,10 +319,10 @@ EOT $url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0'; try { - $this->rfs->getContents('github.com', $url, false); + $this->httpDownloader->get($url); } catch (TransportException $e) { try { - $this->rfs->getContents('github.com', $url, false, array('http' => array('request_fulluri' => false))); + $this->httpDownloader->get($url, array('http' => array('request_fulluri' => false))); } catch (TransportException $e) { return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')'; } @@ -344,7 +344,7 @@ EOT try { $url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/'; - return $this->rfs->getContents($domain, $url, false, array( + return $this->httpDownloader->get($url, array( 'retry-auth-failure' => false, )) ? true : 'Unexpected error'; } catch (\Exception $e) { @@ -374,8 +374,7 @@ EOT } $url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit'; - $json = $this->rfs->getContents($domain, $url, false, array('retry-auth-failure' => false)); - $data = json_decode($json, true); + $data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->parseJson(); return $data['resources']['core']; } @@ -428,7 +427,7 @@ EOT return $result; } - $versionsUtil = new Versions($config, $this->rfs); + $versionsUtil = new Versions($config, $this->httpDownloader); $latest = $versionsUtil->getLatest(); if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') { diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 243755963..903b49d94 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -76,9 +76,9 @@ EOT } $io = $this->getIO(); - $remoteFilesystem = Factory::createRemoteFilesystem($io, $config); + $httpDownloader = Factory::createHttpDownloader($io, $config); - $versionsUtil = new Versions($config, $remoteFilesystem); + $versionsUtil = new Versions($config, $httpDownloader); // switch channel if requested foreach (array('stable', 'preview', 'snapshot') as $channel) { @@ -155,9 +155,9 @@ EOT $io->write(sprintf("Updating to version %s (%s channel).", $updateVersion, $versionsUtil->getChannel())); $remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar'); - $signature = $remoteFilesystem->getContents(self::HOMEPAGE, $remoteFilename.'.sig', false); + $signature = $httpDownloader->get($remoteFilename.'.sig')->getBody(); $io->writeError(' ', false); - $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress')); + $httpDownloader->copy($remoteFilename, $tempFilename); $io->writeError(''); if (!file_exists($tempFilename) || !$signature) { diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index 27b1f4816..86be2d7db 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -123,6 +123,7 @@ class Compiler ->in(__DIR__.'/../../vendor/composer/ca-bundle/') ->in(__DIR__.'/../../vendor/composer/xdebug-handler/') ->in(__DIR__.'/../../vendor/psr/') + ->in(__DIR__.'/../../vendor/react/') ->sort($finderSort) ; diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 00aa499d0..c96fd2188 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -590,18 +590,18 @@ class Factory throw new Exception\NoSslException('The openssl extension is required for SSL/TLS protection but is not available. ' . 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.'); } - $remoteFilesystemOptions = array(); + $httpDownloaderOptions = array(); if ($disableTls === false) { if ($config && $config->get('cafile')) { - $remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile'); + $httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile'); } if ($config && $config->get('capath')) { - $remoteFilesystemOptions['ssl']['capath'] = $config->get('capath'); + $httpDownloaderOptions['ssl']['capath'] = $config->get('capath'); } - $remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options); + $httpDownloaderOptions = array_replace_recursive($httpDownloaderOptions, $options); } try { - $remoteFilesystem = new HttpDownloader($io, $config, $remoteFilesystemOptions, $disableTls); + $httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls); } catch (TransportException $e) { if (false !== strpos($e->getMessage(), 'cafile')) { $io->write('Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.'); @@ -614,7 +614,7 @@ class Factory throw $e; } - return $remoteFilesystem; + return $httpDownloader; } /** diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index b84791420..a61a75c34 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -15,7 +15,7 @@ namespace Composer\Json; use JsonSchema\Validator; use Seld\JsonLint\JsonParser; use Seld\JsonLint\ParsingException; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; @@ -35,25 +35,25 @@ class JsonFile const JSON_UNESCAPED_UNICODE = 256; private $path; - private $rfs; + private $httpDownloader; private $io; /** * Initializes json file reader/parser. * - * @param string $path path to a lockfile - * @param RemoteFilesystem $rfs required for loading http/https json files + * @param string $path path to a lockfile + * @param HttpDownloader $httpDownloader required for loading http/https json files * @param IOInterface $io * @throws \InvalidArgumentException */ - public function __construct($path, RemoteFilesystem $rfs = null, IOInterface $io = null) + public function __construct($path, HttpDownloader $httpDownloader = null, IOInterface $io = null) { $this->path = $path; - if (null === $rfs && preg_match('{^https?://}i', $path)) { - throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed'); + if (null === $httpDownloader && preg_match('{^https?://}i', $path)) { + throw new \InvalidArgumentException('http urls require a HttpDownloader instance to be passed'); } - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; $this->io = $io; } @@ -84,8 +84,8 @@ class JsonFile public function read() { try { - if ($this->rfs) { - $json = $this->rfs->getContents($this->path, $this->path, false); + if ($this->httpDownloader) { + $json = $this->httpDownloader->get($this->path)->getBody(); } else { if ($this->io && $this->io->isDebug()) { $this->io->writeError('Reading ' . $this->path); diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index e6ff84965..1c2fdf986 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -192,7 +192,8 @@ class VersionGuesser } // re-use the HgDriver to fetch branches (this properly includes bookmarks) - $driver = new HgDriver(array('url' => $path), new NullIO(), $this->config, $this->process); + $io = new NullIO(); + $driver = new HgDriver(array('url' => $path), $io, $this->config, new HttpDownloader($io, $this->config), $this->process); $branches = array_keys($driver->getBranches()); // try to find the best (nearest) version branch to assume this feature's version diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index a0036e4fd..b337ea106 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -62,7 +62,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $partialPackagesByName; private $versionParser; - public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher, HttpDownloader $httpDownloader) + public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { parent::__construct(); if (!preg_match('{^[\w.]+\??://}', $repoConfig['url'])) { @@ -101,7 +101,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); $this->versionParser = new VersionParser(); $this->loader = new ArrayLoader($this->versionParser); - if ($httpDownloader && $this->options) { + if ($this->options) { // TODO solve this somehow - should be sent at request time not on the instance $httpDownloader = clone $httpDownloader; $httpDownloader->setOptions($this->options); @@ -781,10 +781,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; while ($retries--) { try { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); - $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $httpDownloader = $this->httpDownloader; - $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); + if ($this->eventDispatcher) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); + } $response = $httpDownloader->get($filename); $json = $response->getBody(); @@ -869,10 +872,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; while ($retries--) { try { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); - $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $httpDownloader = $this->httpDownloader; + + if ($this->eventDispatcher) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); + } - $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); $options = array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))); $response = $httpDownloader->get($filename, $options); $json = $response->getBody(); @@ -925,10 +932,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito protected function asyncFetchFile($filename, $cacheKey, $lastModifiedTime = null) { $retries = 3; - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); - $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $httpDownloader = $this->httpDownloader; + + if ($this->eventDispatcher) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); + } - $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); $io = $this->io; diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index aef5c0381..1bb22c0ed 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -47,7 +47,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn */ private $vendorAlias; - public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher, HttpDownloader $httpDownloader) + public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null) { parent::__construct(); if (!preg_match('{^https?://}', $repoConfig['url'])) { diff --git a/src/Composer/Repository/RepositoryFactory.php b/src/Composer/Repository/RepositoryFactory.php index 515908f64..9508f5886 100644 --- a/src/Composer/Repository/RepositoryFactory.php +++ b/src/Composer/Repository/RepositoryFactory.php @@ -36,7 +36,7 @@ class RepositoryFactory if (0 === strpos($repository, 'http')) { $repoConfig = array('type' => 'composer', 'url' => $repository); } elseif ("json" === pathinfo($repository, PATHINFO_EXTENSION)) { - $json = new JsonFile($repository, Factory::createRemoteFilesystem($io, $config)); + $json = new JsonFile($repository, Factory::createHttpDownloader($io, $config)); $data = $json->read(); if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) { $repoConfig = array('type' => 'composer', 'url' => 'file://' . strtr(realpath($repository), '\\', '/')); @@ -77,7 +77,7 @@ class RepositoryFactory */ public static function createRepo(IOInterface $io, Config $config, array $repoConfig) { - $rm = static::manager($io, $config, null, Factory::createRemoteFilesystem($io, $config)); + $rm = static::manager($io, $config, Factory::createHttpDownloader($io, $config)); $repos = static::createRepos($rm, array($repoConfig)); return reset($repos); @@ -98,7 +98,7 @@ class RepositoryFactory if (!$io) { throw new \InvalidArgumentException('This function requires either an IOInterface or a RepositoryManager'); } - $rm = static::manager($io, $config, null, Factory::createRemoteFilesystem($io, $config)); + $rm = static::manager($io, $config, Factory::createHttpDownloader($io, $config)); } return static::createRepos($rm, $config->getRepositories()); @@ -113,7 +113,7 @@ class RepositoryFactory */ public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { - $rm = new RepositoryManager($io, $config, $eventDispatcher, $httpDownloader); + $rm = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index 8fc01cb08..c3ce0c24a 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -35,12 +35,12 @@ class RepositoryManager private $eventDispatcher; private $httpDownloader; - public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher, HttpDownloader $httpDownloader) + public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { $this->io = $io; $this->config = $config; - $this->eventDispatcher = $eventDispatcher; $this->httpDownloader = $httpDownloader; + $this->eventDispatcher = $eventDispatcher; } /** @@ -127,8 +127,8 @@ class RepositoryManager $reflMethod = new \ReflectionMethod($class, '__construct'); $params = $reflMethod->getParameters(); - if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\HttpDownloader') { - return new $class($config, $this->io, $this->config, $this->eventDispatcher, $this->httpDownloader); + if (isset($params[3]) && $params[3]->getClass() && $params[3]->getClass()->getName() === 'Composer\Util\HttpDownloader') { + return new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher); } return new $class($config, $this->io, $this->config, $this->eventDispatcher); diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index dd69e753a..5770a8326 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -75,8 +75,8 @@ class GitBitbucketDriver extends BitbucketDriver array('url' => $url), $this->io, $this->config, - $this->process, - $this->httpDownloader + $this->httpDownloader, + $this->process ); $this->fallbackDriver->initialize(); } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index f1ad253d8..69eef200d 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -457,8 +457,8 @@ class GitHubDriver extends VcsDriver array('url' => $url), $this->io, $this->config, - $this->process, - $this->httpDownloader + $this->httpDownloader, + $this->process ); $this->gitDriver->initialize(); } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 6a1aa8ac2..1e2775ff7 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -376,8 +376,8 @@ class GitLabDriver extends VcsDriver array('url' => $url), $this->io, $this->config, - $this->process, - $this->httpDownloader + $this->httpDownloader, + $this->process ); $this->gitDriver->initialize(); } diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php index 4a00f2da0..a919e7860 100644 --- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -75,8 +75,8 @@ class HgBitbucketDriver extends BitbucketDriver array('url' => $url), $this->io, $this->config, - $this->process, - $this->httpDownloader + $this->httpDownloader, + $this->process ); $this->fallbackDriver->initialize(); } diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index 17ed706d2..37946da23 100644 --- a/src/Composer/Repository/Vcs/VcsDriver.php +++ b/src/Composer/Repository/Vcs/VcsDriver.php @@ -55,10 +55,10 @@ abstract class VcsDriver implements VcsDriverInterface * @param array $repoConfig The repository configuration * @param IOInterface $io The IO instance * @param Config $config The composer configuration + * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking * @param ProcessExecutor $process Process instance, injectable for mocking - * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking */ - final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null) + final public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, ProcessExecutor $process) { if (Filesystem::isLocalPath($repoConfig['url'])) { $repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']); @@ -69,8 +69,8 @@ abstract class VcsDriver implements VcsDriverInterface $this->repoConfig = $repoConfig; $this->io = $io; $this->config = $config; - $this->process = $process ?: new ProcessExecutor($io); - $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); + $this->httpDownloader = $httpDownloader; + $this->process = $process; } /** diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index d6fb1bbee..d8e4b1501 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -20,6 +20,8 @@ use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Package\Loader\InvalidPackageException; use Composer\Package\Loader\LoaderInterface; use Composer\EventDispatcher\EventDispatcher; +use Composer\Util\ProcessExecutor; +use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; use Composer\Config; @@ -37,6 +39,8 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt protected $type; protected $loader; protected $repoConfig; + protected $httpDownloader; + protected $processExecutor; protected $branchErrorOccurred = false; private $drivers; /** @var VcsDriverInterface */ @@ -44,7 +48,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt /** @var VersionCacheInterface */ private $versionCache; - public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null) + public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null) { parent::__construct(); $this->drivers = $drivers ?: array( @@ -67,6 +71,8 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $this->config = $config; $this->repoConfig = $repoConfig; $this->versionCache = $versionCache; + $this->httpDownloader = $httpDownloader; + $this->processExecutor = new ProcessExecutor($io); } public function getRepoConfig() @@ -87,7 +93,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt if (isset($this->drivers[$this->type])) { $class = $this->drivers[$this->type]; - $this->driver = new $class($this->repoConfig, $this->io, $this->config); + $this->driver = new $class($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; @@ -95,7 +101,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt foreach ($this->drivers as $driver) { if ($driver::supports($this->io, $this->config, $this->url)) { - $this->driver = new $driver($this->repoConfig, $this->io, $this->config); + $this->driver = new $driver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; @@ -104,7 +110,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt foreach ($this->drivers as $driver) { if ($driver::supports($this->io, $this->config, $this->url, true)) { - $this->driver = new $driver($this->repoConfig, $this->io, $this->config); + $this->driver = new $driver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; diff --git a/src/Composer/SelfUpdate/Versions.php b/src/Composer/SelfUpdate/Versions.php index b619bda16..431abecb5 100644 --- a/src/Composer/SelfUpdate/Versions.php +++ b/src/Composer/SelfUpdate/Versions.php @@ -12,7 +12,7 @@ namespace Composer\SelfUpdate; -use Composer\Util\RemoteFilesystem; +use Composer\Util\HttpDownloader; use Composer\Config; use Composer\Json\JsonFile; @@ -21,13 +21,13 @@ use Composer\Json\JsonFile; */ class Versions { - private $rfs; + private $httpDownloader; private $config; private $channel; - public function __construct(Config $config, RemoteFilesystem $rfs) + public function __construct(Config $config, HttpDownloader $httpDownloader) { - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; $this->config = $config; } @@ -62,7 +62,7 @@ class Versions public function getLatest() { $protocol = extension_loaded('openssl') ? 'https' : 'http'; - $versions = JsonFile::parseJson($this->rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/versions', false)); + $versions = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson(); foreach ($versions[$this->getChannel()] as $version) { if ($version['min-php'] <= PHP_VERSION_ID) { diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 631c0df7d..920c5f75a 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -102,6 +102,26 @@ class HttpDownloader return $promise; } + /** + * Retrieve the options set in the constructor + * + * @return array Options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Merges new options + * + * @return array $options + */ + public function setOptions(array $options) + { + $this->options = array_replace_recursive($this->options, $options); + } + private function addJob($request, $sync = false) { $job = array( diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php index ebb1f0456..1b5041d9f 100644 --- a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php +++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\Repository\VcsRepository; use Composer\IO\IOInterface; use Composer\Test\TestCase; +use Composer\Factory; use Composer\Util\Filesystem; /** @@ -96,7 +97,7 @@ class PerforceDownloaderTest extends TestCase { $repository = $this->getMockBuilder('Composer\Repository\VcsRepository') ->setMethods(array('getRepoConfig')) - ->setConstructorArgs(array($repoConfig, $io, $config)) + ->setConstructorArgs(array($repoConfig, $io, $config, Factory::createHttpDownloader($io, $config))) ->getMock(); $repository->expects($this->any())->method('getRepoConfig')->will($this->returnValue($repoConfig)); diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 2e7d70639..acaf8f1ff 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -65,7 +65,7 @@ class InstallerTest extends TestCase $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); - $repositoryManager = new RepositoryManager($io, $config, $eventDispatcher, $httpDownloader); + $repositoryManager = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher); $repositoryManager->setLocalRepository(new InstalledArrayRepository()); if (!is_array($repositories)) { diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 1fe60f589..0ffd70751 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -37,8 +37,8 @@ class ComposerRepositoryTest extends TestCase $repoConfig, new NullIO, FactoryMock::createConfig(), - $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(), - $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock() + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() )) ->getMock(); @@ -203,7 +203,7 @@ class ComposerRepositoryTest extends TestCase ->with($url = 'http://example.org/search.json?q=foo&type=library') ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array()))); - $repository = new ComposerRepository($repoConfig, new NullIO, FactoryMock::createConfig(), $eventDispatcher, $httpDownloader); + $repository = new ComposerRepository($repoConfig, new NullIO, FactoryMock::createConfig(), $httpDownloader, $eventDispatcher); $this->assertSame( array(array('name' => 'foo', 'description' => null)), diff --git a/tests/Composer/Test/Repository/PearRepositoryTest.php b/tests/Composer/Test/Repository/PearRepositoryTest.php index 6046fefb4..867d4978d 100644 --- a/tests/Composer/Test/Repository/PearRepositoryTest.php +++ b/tests/Composer/Test/Repository/PearRepositoryTest.php @@ -28,7 +28,7 @@ class PearRepositoryTest extends TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $remoteFilesystem; + private $httpDownloader; public function testComposerShouldSetIncludePath() { diff --git a/tests/Composer/Test/Repository/RepositoryManagerTest.php b/tests/Composer/Test/Repository/RepositoryManagerTest.php index c4f09de87..35afd91e2 100644 --- a/tests/Composer/Test/Repository/RepositoryManagerTest.php +++ b/tests/Composer/Test/Repository/RepositoryManagerTest.php @@ -38,8 +38,8 @@ class RepositoryManagerTest extends TestCase $rm = new RepositoryManager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getMockBuilder('Composer\Config')->getMock(), - $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(), - $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock() + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); $repository1 = $this->getMockBuilder('Composer\Repository\RepositoryInterface')->getMock(); @@ -62,8 +62,8 @@ class RepositoryManagerTest extends TestCase $rm = new RepositoryManager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $config = $this->getMockBuilder('Composer\Config')->setMethods(array('get'))->getMock(), - $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(), - $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock() + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); $tmpdir = $this->tmpdir; diff --git a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php index a3a9219d9..f0139970b 100644 --- a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php @@ -16,6 +16,7 @@ use Composer\Config; use Composer\Repository\Vcs\GitBitbucketDriver; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\ProcessExecutor; use Composer\Util\Http\Response; /** @@ -68,8 +69,8 @@ class GitBitbucketDriverTest extends TestCase $repoConfig, $this->io, $this->config, - null, - $this->httpDownloader + $this->httpDownloader, + new ProcessExecutor($this->io) ); $driver->initialize(); diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index 1f721a4c7..977a4a7aa 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -96,7 +96,7 @@ class GitHubDriverTest extends TestCase 'url' => $repoUrl, ); - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $process, $httpDownloader); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -139,7 +139,11 @@ class GitHubDriverTest extends TestCase ); $repoUrl = 'https://github.com/composer/packagist.git'; - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $httpDownloader); + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor') + ->disableOriginalConstructor() + ->getMock(); + + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -192,7 +196,11 @@ class GitHubDriverTest extends TestCase ); $repoUrl = 'https://github.com/composer/packagist.git'; - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $httpDownloader); + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor') + ->disableOriginalConstructor() + ->getMock(); + + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config,$httpDownloader, $process); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); @@ -279,7 +287,7 @@ class GitHubDriverTest extends TestCase 'url' => $repoUrl, ); - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $process, $httpDownloader); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); diff --git a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php index f940733ae..0fd2fa956 100644 --- a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php @@ -92,7 +92,7 @@ JSON; ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -129,7 +129,7 @@ JSON; ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -165,7 +165,7 @@ JSON; ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -204,7 +204,7 @@ JSON; $this->mockResponse($apiUrl, array(), sprintf($projectData, $domain, $port, $namespace)) ->shouldBeCalledTimes(1); - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -457,7 +457,7 @@ JSON; ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -488,7 +488,7 @@ JSON; ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -519,7 +519,7 @@ JSON; ->shouldBeCalledTimes(1) ; - $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->process->reveal(), $this->httpDownloader->reveal()); + $driver = new GitLabDriver(array('url' => $url), $this->io->reveal(), $this->config, $this->httpDownloader->reveal(), $this->process->reveal()); $driver->initialize(); $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); @@ -555,8 +555,8 @@ JSON; array('url' => 'https://gitlab.mycompany.local/mygroup/myproject', 'options' => $options), $this->io->reveal(), $this->config, - $this->process->reveal(), - $this->httpDownloader->reveal() + $this->httpDownloader->reveal(), + $this->process->reveal() ); $driver->initialize(); } diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php index 8225a4a54..ff4e19121 100644 --- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -45,7 +45,7 @@ class PerforceDriverTest extends TestCase $this->process = $this->getMockProcessExecutor(); $this->httpDownloader = $this->getMockHttpDownloader(); $this->perforce = $this->getMockPerforce(); - $this->driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->httpDownloader); + $this->driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->process); $this->overrideDriverInternalPerforce($this->perforce); } @@ -113,7 +113,7 @@ class PerforceDriverTest extends TestCase public function testInitializeCapturesVariablesFromRepoConfig() { - $driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->httpDownloader); + $driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); $this->assertEquals(self::TEST_URL, $driver->getUrl()); $this->assertEquals(self::TEST_DEPOT, $driver->getDepot()); diff --git a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php index 029d20160..946c198f2 100644 --- a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php @@ -46,6 +46,7 @@ class SvnDriverTest extends TestCase public function testWrongCredentialsInUrl() { $console = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); $output = "svn: OPTIONS of 'https://corp.svn.local/repo':"; $output .= " authorization failed: Could not authenticate to server:"; @@ -66,7 +67,7 @@ class SvnDriverTest extends TestCase 'url' => 'https://till:secret@corp.svn.local/repo', ); - $svn = new SvnDriver($repoConfig, $console, $this->config, $process); + $svn = new SvnDriver($repoConfig, $console, $this->config, $httpDownloader, $process); $svn->initialize(); } From 1cd9f4f9dbd5a2830e116b427076ce6c3d7f4b16 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 12 Nov 2018 15:35:46 +0100 Subject: [PATCH 028/321] Disable request_fulluri by default for HTTPS connections --- src/Composer/Util/StreamContextFactory.php | 6 +++--- tests/Composer/Test/Util/StreamContextFactoryTest.php | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 72d12115d..b25b307a1 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -87,15 +87,15 @@ final class StreamContextFactory // enabled request_fulluri unless it is explicitly disabled switch (parse_url($url, PHP_URL_SCHEME)) { - case 'http': // default request_fulluri to true + case 'http': // default request_fulluri to true for HTTP $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI'); if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { $options['http']['request_fulluri'] = true; } break; - case 'https': // default request_fulluri to true + case 'https': // default request_fulluri to false for HTTPS $reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI'); - if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { + if (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv) { $options['http']['request_fulluri'] = true; } break; diff --git a/tests/Composer/Test/Util/StreamContextFactoryTest.php b/tests/Composer/Test/Util/StreamContextFactoryTest.php index 9bb04aaa1..3d60a9a38 100644 --- a/tests/Composer/Test/Util/StreamContextFactoryTest.php +++ b/tests/Composer/Test/Util/StreamContextFactoryTest.php @@ -142,7 +142,6 @@ class StreamContextFactoryTest extends TestCase $expected = array( 'http' => array( 'proxy' => 'tcp://proxyserver.net:80', - 'request_fulluri' => true, 'method' => 'GET', 'header' => array('User-Agent: foo', "Proxy-Authorization: Basic " . base64_encode('username:password')), 'max_redirects' => 20, @@ -173,7 +172,6 @@ class StreamContextFactoryTest extends TestCase $expected = array( 'http' => array( 'proxy' => 'ssl://woopproxy.net:443', - 'request_fulluri' => true, 'method' => 'GET', 'max_redirects' => 20, 'follow_location' => 1, From 09fd239f243e53e989568802a18d5643f7ff2091 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 12 Nov 2018 16:19:06 +0100 Subject: [PATCH 029/321] Fix factory test --- src/Composer/Util/HttpDownloader.php | 1 + tests/Composer/Test/FactoryTest.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 920c5f75a..d07823ec0 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -32,6 +32,7 @@ class HttpDownloader private $io; private $config; private $jobs = array(); + private $options = array(); private $index; private $progress; private $lastProgress; diff --git a/tests/Composer/Test/FactoryTest.php b/tests/Composer/Test/FactoryTest.php index 6704e5b15..96b0e95d5 100644 --- a/tests/Composer/Test/FactoryTest.php +++ b/tests/Composer/Test/FactoryTest.php @@ -35,6 +35,6 @@ class FactoryTest extends TestCase ->with($this->equalTo('disable-tls')) ->will($this->returnValue(true)); - Factory::createRemoteFilesystem($ioMock, $config); + Factory::createHttpDownloader($ioMock, $config); } } From 346de47af23cbc8441bd1683491d5c25592b8a42 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 12 Nov 2018 17:21:23 +0100 Subject: [PATCH 030/321] Small fixes --- src/Composer/Command/CreateProjectCommand.php | 2 +- .../Repository/ComposerRepository.php | 25 +++++++++++++++++-- .../Repository/RepositoryInterface.php | 11 ++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 1b58d59c5..a1c364539 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -374,7 +374,7 @@ EOT { $factory = new Factory(); - return $factory->createDownloadManager($io, $config); + return $factory->createDownloadManager($io, $config, $factory->createHttpDownloader($io, $config)); } protected function createInstallationManager() diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index b337ea106..7b916d95a 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -28,6 +28,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Downloader\TransportException; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\EmptyConstraint; /** * @author Jordi Boggiano @@ -156,9 +157,19 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito */ public function findPackages($name, $constraint = null) { - if (!$this->hasProviders()) { + // this call initializes loadRootServerFile which is needed for the rest below to work + $hasProviders = $this->hasProviders(); + + // TODO we need a new way for the repo to report this v2 protocol somehow + if ($this->lazyProvidersUrl) { + return $this->loadAsyncPackages(array($name => new EmptyConstraint()), function ($name, $stability) { + return true; + }); + } + if (!$hasProviders) { return parent::findPackages($name, $constraint); } + // normalize name $name = strtolower($name); @@ -197,10 +208,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable) { + // this call initializes loadRootServerFile which is needed for the rest below to work + $hasProviders = $this->hasProviders(); + + // TODO we need a new way for the repo to report this v2 protocol somehow if ($this->lazyProvidersUrl) { return $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable); } - if (!$this->hasProviders()) { + if (!$hasProviders) { return parent::loadPackages($packageNameMap, $isPackageAcceptableCallable); } @@ -225,6 +240,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $packages = array_merge($packages, $matches); } + return $packages; } @@ -528,6 +544,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->lazyProvidersUrl) { foreach ($packageNames as $name => $constraint) { + // skip platform packages, root package and composer-plugin-api + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { + continue; + } + $url = str_replace('%package%', $name, $this->lazyProvidersUrl); $cacheKey = 'provider-'.strtr($name, '/', '$').'.json'; diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 55b76d33d..567455163 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -13,6 +13,7 @@ namespace Composer\Repository; use Composer\Package\PackageInterface; +use Composer\Semver\Constraint\ConstraintInterface; /** * Repository interface. @@ -38,8 +39,8 @@ interface RepositoryInterface extends \Countable /** * Searches for the first match of a package by name and version. * - * @param string $name package name - * @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against + * @param string $name package name + * @param string|ConstraintInterface $constraint package version or version constraint to match against * * @return PackageInterface|null */ @@ -48,8 +49,8 @@ interface RepositoryInterface extends \Countable /** * Searches for all packages matching a name and optionally a version. * - * @param string $name package name - * @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against + * @param string $name package name + * @param string|ConstraintInterface $constraint package version or version constraint to match against * * @return PackageInterface[] */ @@ -66,7 +67,7 @@ interface RepositoryInterface extends \Countable /** * Returns list of registered packages with the supplied name * - * @param bool[] $packageNameMap + * @param ConstraintInterface[] $packageNameMap package names pointing to constraints * @param $isPackageAcceptableCallable * @return PackageInterface[] */ From 655a784fac39230dcc5065aa37888ff614e6f5d4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 12 Nov 2018 17:44:18 +0100 Subject: [PATCH 031/321] Fix findPackage(s) implementation --- src/Composer/Repository/ComposerRepository.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 7b916d95a..dc9f5a79c 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -127,15 +127,24 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito */ public function findPackage($name, $constraint) { - if (!$this->hasProviders()) { - return parent::findPackage($name, $constraint); - } + // this call initializes loadRootServerFile which is needed for the rest below to work + $hasProviders = $this->hasProviders(); $name = strtolower($name); if (!$constraint instanceof ConstraintInterface) { $constraint = $this->versionParser->parseConstraints($constraint); } + // TODO we need a new way for the repo to report this v2 protocol somehow + if ($this->lazyProvidersUrl) { + return $this->loadAsyncPackages(array($name => $constraint), function ($name, $stability) { + return true; + }); + } + if (!$hasProviders) { + return parent::findPackage($name, $constraint); + } + foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { $packages = $this->whatProvides($providerName); @@ -162,7 +171,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito // TODO we need a new way for the repo to report this v2 protocol somehow if ($this->lazyProvidersUrl) { - return $this->loadAsyncPackages(array($name => new EmptyConstraint()), function ($name, $stability) { + return $this->loadAsyncPackages(array($name => $constraint ?: new EmptyConstraint()), function ($name, $stability) { return true; }); } From 4a8a1cb0c9d462c715b01a765df3bbc3b9ae69de Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 12 Nov 2018 17:58:28 +0100 Subject: [PATCH 032/321] Fix PHP 5.3 support --- .../Repository/ComposerRepository.php | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index dc9f5a79c..8c92d3e4d 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -529,28 +529,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $packages = array(); $repo = $this; - $createPackageIfAcceptable = function ($version, $constraint) use (&$packages, $isPackageAcceptableCallable, $repo) { - if (!call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { - return; - } - - if (isset($version['version_normalized']) && $constraint && !$constraint->matches(new Constraint('==', $version['version_normalized']))) { - return; - } - - // load acceptable packages in the providers - $package = $this->createPackage($version, 'Composer\Package\CompletePackage'); - $package->setRepository($repo); - - // if there was no version_normalized, then we need to check now for the constraint - if (!$constraint || isset($version['version_normalized']) || $constraint->matches(new Constraint('==', $package->getVersion()))) { - $packages[spl_object_hash($package)] = $package; - if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { - $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); - } - } - }; - + // TODO what if not, then throw? if ($this->lazyProvidersUrl) { foreach ($packageNames as $name => $constraint) { // skip platform packages, root package and composer-plugin-api @@ -568,7 +547,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $this->asyncFetchFile($url, $cacheKey, $lastModified) - ->then(function ($response) use (&$packages, $contents, $name, $constraint, $createPackageIfAcceptable) { + ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) { if (true === $response) { $response = $contents; } @@ -591,10 +570,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $unpackedVersion[$key] = $version[$key.'s'][$index]; } - $createPackageIfAcceptable($unpackedVersion, $constraint); + $repo->createPackageIfAcceptable($packages, $isPackageAcceptableCallable, $unpackedVersion, $constraint); } } else { - $createPackageIfAcceptable($version, $constraint); + $repo->createPackageIfAcceptable($packages, $isPackageAcceptableCallable, $version, $constraint); } } }, function ($e) { @@ -611,6 +590,34 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito // RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed } + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * + * @private + */ + public function createPackageIfAcceptable(&$packages, $isPackageAcceptableCallable, $version, $constraint) + { + if (!call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { + return; + } + + if (isset($version['version_normalized']) && $constraint && !$constraint->matches(new Constraint('==', $version['version_normalized']))) { + return; + } + + // load acceptable packages in the providers + $package = $this->createPackage($version, 'Composer\Package\CompletePackage'); + $package->setRepository($this); + + // if there was no version_normalized, then we need to check now for the constraint + if (!$constraint || isset($version['version_normalized']) || $constraint->matches(new Constraint('==', $package->getVersion()))) { + $packages[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { + $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + } + } + } + protected function loadRootServerFile() { if (null !== $this->rootData) { From fd11cf3618d960e4fd00e891b9e260a42ed92e0b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 14 Nov 2018 17:54:19 +0100 Subject: [PATCH 033/321] Port/extract most behavior of RemoteFilesystem to CurlDownloader --- src/Composer/Composer.php | 1 + src/Composer/Util/AuthHelper.php | 169 ++++++++++ src/Composer/Util/Http/CurlDownloader.php | 317 +++++++++++++----- src/Composer/Util/Http/Response.php | 22 +- src/Composer/Util/HttpDownloader.php | 35 +- src/Composer/Util/RemoteFilesystem.php | 197 +---------- src/Composer/Util/StreamContextFactory.php | 56 +++- .../Test/Util/RemoteFilesystemTest.php | 40 ++- 8 files changed, 518 insertions(+), 319 deletions(-) diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index a3972f44f..c1a61545c 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -32,6 +32,7 @@ class Composer const VERSION = '@package_version@'; const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@'; const RELEASE_DATE = '@release_date@'; + const SOURCE_VERSION = '2.0-source'; /** * @var Package\RootPackageInterface diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index 72b23ba22..e80a5b0c3 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -14,6 +14,7 @@ namespace Composer\Util; use Composer\Config; use Composer\IO\IOInterface; +use Composer\Downloader\TransportException; /** * @author Jordi Boggiano @@ -60,4 +61,172 @@ class AuthHelper ); } } + + + public function promptAuthIfNeeded($url, $origin, $httpStatus, $reason = null, $warning = null, $headers = array()) + { + $storeAuth = false; + $retry = false; + + if (in_array($origin, $this->config->get('github-domains'), true)) { + $gitHubUtil = new GitHub($this->io, $this->config, null); + $message = "\n"; + + $rateLimited = $gitHubUtil->isRateLimited($headers); + if ($rateLimited) { + $rateLimit = $gitHubUtil->getRateLimit($headers); + if ($this->io->hasAuthentication($origin)) { + $message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.'; + } else { + $message = 'Create a GitHub OAuth token to go over the API rate limit.'; + } + + $message = sprintf( + 'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$url.'. '.$message.' You can also wait until %s for the rate limit to reset.', + $rateLimit['limit'], + $rateLimit['reset'] + )."\n"; + } else { + $message .= 'Could not fetch '.$url.', please '; + if ($this->io->hasAuthentication($origin)) { + $message .= 'review your configured GitHub OAuth token or enter a new one to access private repos'; + } else { + $message .= 'create a GitHub OAuth token to access private repos'; + } + } + + if (!$gitHubUtil->authorizeOAuth($origin) + && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($origin, $message)) + ) { + throw new TransportException('Could not authenticate against '.$origin, 401); + } + } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) { + $message = "\n".'Could not fetch '.$url.', enter your ' . $origin . ' credentials ' .($httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit'); + $gitLabUtil = new GitLab($this->io, $this->config, null); + + if ($this->io->hasAuthentication($origin) && ($auth = $this->io->getAuthentication($origin)) && $auth['password'] === 'private-token') { + throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $httpStatus); + } + + if (!$gitLabUtil->authorizeOAuth($origin) + && (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively(parse_url($url, PHP_URL_SCHEME), $origin, $message)) + ) { + throw new TransportException('Could not authenticate against '.$origin, 401); + } + } elseif ($origin === 'bitbucket.org') { + $askForOAuthToken = true; + if ($this->io->hasAuthentication($origin)) { + $auth = $this->io->getAuthentication($origin); + if ($auth['username'] !== 'x-token-auth') { + $bitbucketUtil = new Bitbucket($this->io, $this->config); + $accessToken = $bitbucketUtil->requestToken($origin, $auth['username'], $auth['password']); + if (!empty($accessToken)) { + $this->io->setAuthentication($origin, 'x-token-auth', $accessToken); + $askForOAuthToken = false; + } + } else { + throw new TransportException('Could not authenticate against ' . $origin, 401); + } + } + + if ($askForOAuthToken) { + $message = "\n".'Could not fetch ' . $url . ', please create a bitbucket OAuth token to ' . (($httpStatus === 401 || $httpStatus === 403) ? 'access private repos' : 'go over the API rate limit'); + $bitBucketUtil = new Bitbucket($this->io, $this->config); + if (! $bitBucketUtil->authorizeOAuth($origin) + && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($origin, $message)) + ) { + throw new TransportException('Could not authenticate against ' . $origin, 401); + } + } + } else { + // 404s are only handled for github + if ($httpStatus === 404) { + return; + } + + // fail if the console is not interactive + if (!$this->io->isInteractive()) { + if ($httpStatus === 401) { + $message = "The '" . $url . "' URL required authentication.\nYou must be using the interactive console to authenticate"; + } + if ($httpStatus === 403) { + $message = "The '" . $url . "' URL could not be accessed: " . $reason; + } + + throw new TransportException($message, $httpStatus); + } + // fail if we already have auth + if ($this->io->hasAuthentication($origin)) { + throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $httpStatus); + } + + $this->io->overwriteError(''); + if ($warning) { + $this->io->writeError(' '.$warning.''); + } + $this->io->writeError(' Authentication required ('.parse_url($url, PHP_URL_HOST).'):'); + $username = $this->io->ask(' Username: '); + $password = $this->io->askAndHideAnswer(' Password: '); + $this->io->setAuthentication($origin, $username, $password); + $storeAuth = $this->config->get('store-auths'); + } + + $retry = true; + + return array('retry' => $retry, 'storeAuth' => $storeAuth); + } + + public function addAuthenticationHeader(array $headers, $origin, $url) + { + if ($this->io->hasAuthentication($origin)) { + $auth = $this->io->getAuthentication($origin); + if ('github.com' === $origin && 'x-oauth-basic' === $auth['password']) { + $headers[] = 'Authorization: token '.$auth['username']; + } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) { + if ($auth['password'] === 'oauth2') { + $headers[] = 'Authorization: Bearer '.$auth['username']; + } elseif ($auth['password'] === 'private-token') { + $headers[] = 'PRIVATE-TOKEN: '.$auth['username']; + } + } elseif ( + 'bitbucket.org' === $origin + && $url !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL + && 'x-token-auth' === $auth['username'] + ) { + if (!$this->isPublicBitBucketDownload($url)) { + $headers[] = 'Authorization: Bearer ' . $auth['password']; + } + } else { + $authStr = base64_encode($auth['username'] . ':' . $auth['password']); + $headers[] = 'Authorization: Basic '.$authStr; + } + } + + return $headers; + } + + /** + * @link https://github.com/composer/composer/issues/5584 + * + * @param string $urlToBitBucketFile URL to a file at bitbucket.org. + * + * @return bool Whether the given URL is a public BitBucket download which requires no authentication. + */ + public function isPublicBitBucketDownload($urlToBitBucketFile) + { + $domain = parse_url($urlToBitBucketFile, PHP_URL_HOST); + if (strpos($domain, 'bitbucket.org') === false) { + // Bitbucket downloads are hosted on amazonaws. + // We do not need to authenticate there at all + return true; + } + + $path = parse_url($urlToBitBucketFile, PHP_URL_PATH); + + // Path for a public download follows this pattern /{user}/{repo}/downloads/{whatever} + // {@link https://blog.bitbucket.org/2009/04/12/new-feature-downloads/} + $pathParts = explode('/', $path); + + return count($pathParts) >= 4 && $pathParts[3] == 'downloads'; + } } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 846c41883..2accb7a0c 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -16,6 +16,9 @@ use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; +use Composer\Util\RemoteFilesystem; +use Composer\Util\StreamContextFactory; +use Composer\Util\AuthHelper; use Psr\Log\LoggerInterface; use React\Promise\Promise; @@ -28,8 +31,14 @@ class CurlDownloader private $multiHandle; private $shareHandle; private $jobs = array(); + /** @var IOInterface */ private $io; + /** @var Config */ + private $config; + /** @var AuthHelper */ + private $authHelper; private $selectTimeout = 5.0; + private $maxRedirects = 20; protected $multiErrors = array( CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'), CURLM_BAD_EASY_HANDLE => array('CURLM_BAD_EASY_HANDLE', "An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle."), @@ -42,6 +51,7 @@ class CurlDownloader 'method' => CURLOPT_CUSTOMREQUEST, 'content' => CURLOPT_POSTFIELDS, 'proxy' => CURLOPT_PROXY, + 'header' => CURLOPT_HTTPHEADER, ), 'ssl' => array( 'ciphers' => CURLOPT_SSL_CIPHER_LIST, @@ -62,6 +72,7 @@ class CurlDownloader public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) { $this->io = $io; + $this->config = $config; $this->multiHandle = $mh = curl_multi_init(); if (function_exists('curl_multi_setopt')) { @@ -77,79 +88,112 @@ class CurlDownloader curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); } + + $this->authHelper = new AuthHelper($io, $config); } public function download($resolve, $reject, $origin, $url, $options, $copyTo = null) { - $ch = curl_init(); - $hd = fopen('php://temp/maxmemory:32768', 'w+b'); + return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo); + } - // TODO auth & other context - // TODO cleanup + private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array()) + { + // TODO allow setting attributes somehow + $attributes = array_merge(array( + 'retryAuthFailure' => true, + 'redirects' => 1, + 'storeAuth' => false, + ), $attributes); - if ($copyTo && !$fd = @fopen($copyTo.'~', 'w+b')) { - // TODO throw here probably? - $copyTo = null; + $originalOptions = $options; + + // check URL can be accessed (i.e. is not insecure) + $this->config->prohibitUrlByConfig($url, $this->io); + + $curlHandle = curl_init(); + $headerHandle = fopen('php://temp/maxmemory:32768', 'w+b'); + + if ($copyTo) { + $errorMessage = ''; + set_error_handler(function ($code, $msg) use (&$errorMessage) { + if ($errorMessage) { + $errorMessage .= "\n"; + } + $errorMessage .= preg_replace('{^fopen\(.*?\): }', '', $msg); + }); + $bodyHandle = fopen($copyTo.'~', 'w+b'); + restore_error_handler(); + if (!$bodyHandle) { + throw new TransportException('The "'.$url.'" file could not be written to '.$copyTo.': '.$errorMessage); + } + } else { + $bodyHandle = @fopen('php://temp/maxmemory:524288', 'w+b'); } - if (!$copyTo) { - $fd = @fopen('php://temp/maxmemory:524288', 'w+b'); + + curl_setopt($curlHandle, CURLOPT_URL, $url); + curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curlHandle, CURLOPT_MAXREDIRS, 20); + //curl_setopt($curlHandle, CURLOPT_DNS_USE_GLOBAL_CACHE, false); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($curlHandle, CURLOPT_TIMEOUT, 10); // TODO increase + curl_setopt($curlHandle, CURLOPT_WRITEHEADER, $headerHandle); + curl_setopt($curlHandle, CURLOPT_FILE, $bodyHandle); + curl_setopt($curlHandle, CURLOPT_ENCODING, "gzip"); + curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS); + if (defined('CURLOPT_SSL_FALSESTART')) { + curl_setopt($curlHandle, CURLOPT_SSL_FALSESTART, true); + } + if (function_exists('curl_share_init')) { + curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle); } if (!isset($options['http']['header'])) { $options['http']['header'] = array(); } - $headers = array_diff($options['http']['header'], array('Connection: close')); + $options['http']['header'] = array_diff($options['http']['header'], array('Connection: close')); + $options['http']['header'][] = 'Connection: keep-alive'; - // TODO - $degradedMode = false; - if ($degradedMode) { - curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); - } else { - $headers[] = 'Connection: keep-alive'; - $version = curl_version(); - $features = $version['features']; - if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) { - curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); - } + $version = curl_version(); + $features = $version['features']; + if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) { + curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); } - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - //curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); // TODO increase - curl_setopt($ch, CURLOPT_WRITEHEADER, $hd); - curl_setopt($ch, CURLOPT_FILE, $fd); - if (function_exists('curl_share_init')) { - curl_setopt($ch, CURLOPT_SHARE, $this->shareHandle); - } + $options['http']['header'] = $this->authHelper->addAuthenticationHeader($options['http']['header'], $origin, $url); + $options = StreamContextFactory::initOptions($url, $options); foreach (self::$options as $type => $curlOptions) { foreach ($curlOptions as $name => $curlOption) { if (isset($options[$type][$name])) { - curl_setopt($ch, $curlOption, $options[$type][$name]); + curl_setopt($curlHandle, $curlOption, $options[$type][$name]); } } } - $progress = array_diff_key(curl_getinfo($ch), self::$timeInfo); + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); - $this->jobs[(int) $ch] = array( + $this->jobs[(int) $curlHandle] = array( + 'url' => $url, + 'origin' => $origin, + 'attributes' => $attributes, + 'options' => $originalOptions, 'progress' => $progress, - 'ch' => $ch, + 'curlHandle' => $curlHandle, //'callback' => $params['notification'], - 'file' => $copyTo, - 'hd' => $hd, - 'fd' => $fd, + 'filename' => $copyTo, + 'headerHandle' => $headerHandle, + 'bodyHandle' => $bodyHandle, 'resolve' => $resolve, 'reject' => $reject, ); - $this->io->write('Downloading '.$url, true, IOInterface::DEBUG); + $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : ''; + $ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : ''; + $this->io->writeError('Downloading ' . $url . $usingProxy . $ifModified, true, IOInterface::DEBUG); - $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $ch)); + $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); //$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false); } @@ -169,74 +213,114 @@ class CurlDownloader } while ($progress = curl_multi_info_read($this->multiHandle)) { - $h = $progress['handle']; - $i = (int) $h; + $curlHandle = $progress['handle']; + $i = (int) $curlHandle; if (!isset($this->jobs[$i])) { continue; } - $progress = array_diff_key(curl_getinfo($h), self::$timeInfo); + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); $job = $this->jobs[$i]; unset($this->jobs[$i]); - curl_multi_remove_handle($this->multiHandle, $h); - $error = curl_error($h); - $errno = curl_errno($h); - curl_close($h); + curl_multi_remove_handle($this->multiHandle, $curlHandle); + $error = curl_error($curlHandle); + $errno = curl_errno($curlHandle); + curl_close($curlHandle); + $headers = null; + $statusCode = null; + $response = null; try { - //$this->onProgress($h, $job['callback'], $progress, $job['progress']); - if ('' !== $error) { - throw new TransportException(curl_error($h)); + //$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']); + if (CURLE_OK !== $errno) { + throw new TransportException($error); } - if ($job['file']) { - if (CURLE_OK === $errno) { - fclose($job['fd']); - rename($job['file'].'~', $job['file']); - call_user_func($job['resolve'], true); - } - // TODO otherwise show error? + $statusCode = $progress['http_code']; + rewind($job['headerHandle']); + $headers = explode("\r\n", rtrim(stream_get_contents($job['headerHandle']))); + fclose($job['headerHandle']); + + // prepare response object + if ($job['filename']) { + fclose($job['bodyHandle']); + $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $job['filename'].'~'); } else { - rewind($job['hd']); - $headers = explode("\r\n", rtrim(stream_get_contents($job['hd']))); - fclose($job['hd']); - rewind($job['fd']); - $contents = stream_get_contents($job['fd']); - fclose($job['fd']); - $this->io->writeError('['.$progress['http_code'].'] '.$progress['url'], true, IOInterface::DEBUG); - call_user_func($job['resolve'], new Response(array('url' => $progress['url']), $progress['http_code'], $headers, $contents)); + rewind($job['bodyHandle']); + $contents = stream_get_contents($job['bodyHandle']); + fclose($job['bodyHandle']); + $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); + $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); } - } catch (TransportException $e) { - fclose($job['hd']); - fclose($job['fd']); - if ($job['file']) { - @unlink($job['file'].'~'); + + $response = $this->retryIfAuthNeeded($job, $response); + + // handle 3xx redirects, 304 Not Modified is excluded + if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['redirects'] < $this->maxRedirects) { + // TODO + $response = $this->handleRedirect($job, $response); + } + + // fail 4xx and 5xx responses and capture the response + if ($statusCode >= 400 && $statusCode <= 599) { + throw $this->failResponse($job, $response, $response->getStatusMessage()); +// $this->io->overwriteError("Downloading (failed)", false); + } + + if ($job['attributes']['storeAuth']) { + $this->authHelper->storeAuth($job['origin'], $job['attributes']['storeAuth']); + } + + // resolve promise + if ($job['filename']) { + rename($job['filename'].'~', $job['filename']); + call_user_func($job['resolve'], true); + } else { + call_user_func($job['resolve'], $response); + } + } catch (\Exception $e) { + if ($e instanceof TransportException && $headers) { + $e->setHeaders($headers); + $e->setStatusCode($statusCode); + } + if ($e instanceof TransportException && $response) { + $e->setResponse($response->getBody()); + } + + if (is_resource($job['headerHandle'])) { + fclose($job['headerHandle']); + } + if (is_resource($job['bodyHandle'])) { + fclose($job['bodyHandle']); + } + if ($job['filename']) { + @unlink($job['filename'].'~'); } call_user_func($job['reject'], $e); } } - foreach ($this->jobs as $i => $h) { + foreach ($this->jobs as $i => $curlHandle) { if (!isset($this->jobs[$i])) { continue; } - $h = $this->jobs[$i]['ch']; - $progress = array_diff_key(curl_getinfo($h), self::$timeInfo); + $curlHandle = $this->jobs[$i]['curlHandle']; + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); if ($this->jobs[$i]['progress'] !== $progress) { $previousProgress = $this->jobs[$i]['progress']; $this->jobs[$i]['progress'] = $progress; try { - //$this->onProgress($h, $this->jobs[$i]['callback'], $progress, $previousProgress); + //$this->onProgress($curlHandle, $this->jobs[$i]['callback'], $progress, $previousProgress); } catch (TransportException $e) { var_dump('Caught '.$e->getMessage());die; unset($this->jobs[$i]); - curl_multi_remove_handle($this->multiHandle, $h); - curl_close($h); + curl_multi_remove_handle($this->multiHandle, $curlHandle); + curl_close($curlHandle); - fclose($job['hd']); - fclose($job['fd']); - if ($job['file']) { - @unlink($job['file'].'~'); + fclose($job['headerHandle']); + fclose($job['bodyHandle']); + if ($job['filename']) { + @unlink($job['filename'].'~'); } call_user_func($job['reject'], $e); } @@ -245,22 +329,77 @@ class CurlDownloader } catch (\Exception $e) { var_dump('Caught2', get_class($e), $e->getMessage(), $e);die; } - -// TODO finalize / resolve -// if ($copyTo && !isset($this->exceptions[(int) $ch])) { -// $fd = fopen($copyTo, 'rb'); -// } -// } - private function onProgress($ch, callable $notify, array $progress, array $previousProgress) + private function retryIfAuthNeeded(array $job, Response $response) + { + if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) { + $warning = null; + if ($response->getHeader('content-type') === 'application/json') { + $data = json_decode($response->getBody(), true); + if (!empty($data['warning'])) { + $warning = $data['warning']; + } + } + + $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $warning, $response->getHeaders()); + + if ($result['retry']) { + // TODO retry somehow using $result['storeAuth'] in the attributes + } + } + + $locationHeader = $response->getHeader('location'); + $needsAuthRetry = false; + + // check for bitbucket login page asking to authenticate + if ( + $job['origin'] === 'bitbucket.org' + && !$this->authHelper->isPublicBitBucketDownload($job['url']) + && substr($job['url'], -4) === '.zip' + && (!$locationHeader || substr($locationHeader, -4) !== '.zip') + && preg_match('{^text/html\b}i', $response->getHeader('content-type')) + ) { + $needsAuthRetry = 'Bitbucket requires authentication and it was not provided'; + } + + // check for gitlab 404 when downloading archives + if ( + $response->getStatusCode() === 404 + && $this->config && in_array($job['origin'], $this->config->get('gitlab-domains'), true) + && false !== strpos($job['url'], 'archive.zip') + ) { + $needsAuthRetry = 'GitLab requires authentication and it was not provided'; + } + + if ($needsAuthRetry) { + if ($job['attributes']['retryAuthFailure']) { + $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401); + if ($result['retry']) { + // TODO ... + // TODO return early here to abort failResponse + } + } + + throw $this->failResponse($job, $response, $needsAuthRetry); + } + + return $response; + } + + private function failResponse(array $job, Response $response, $errorMessage) + { + return new TransportException('The "'.$job['url'].'" file could not be downloaded ('.$errorMessage.')', $response->getStatusCode()); + } + + private function onProgress($curlHandle, callable $notify, array $progress, array $previousProgress) { if (300 <= $progress['http_code'] && $progress['http_code'] < 400) { return; } if (!$previousProgress['http_code'] && $progress['http_code'] && $progress['http_code'] < 200 || 400 <= $progress['http_code']) { $code = 403 === $progress['http_code'] ? STREAM_NOTIFY_AUTH_RESULT : STREAM_NOTIFY_FAILURE; - $notify($code, STREAM_NOTIFY_SEVERITY_ERR, curl_error($ch), $progress['http_code'], 0, 0, false); + $notify($code, STREAM_NOTIFY_SEVERITY_ERR, curl_error($curlHandle), $progress['http_code'], 0, 0, false); } if ($previousProgress['download_content_length'] < $progress['download_content_length']) { $notify(STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['download_content_length'], false); diff --git a/src/Composer/Util/Http/Response.php b/src/Composer/Util/Http/Response.php index f76057f3e..d2774c938 100644 --- a/src/Composer/Util/Http/Response.php +++ b/src/Composer/Util/Http/Response.php @@ -27,7 +27,7 @@ class Response throw new \LogicException('url key missing from request array'); } $this->request = $request; - $this->code = $code; + $this->code = (int) $code; $this->headers = $headers; $this->body = $body; } @@ -37,6 +37,23 @@ class Response return $this->code; } + /** + * @return string|null + */ + public function getStatusMessage() + { + $value = null; + foreach ($this->headers as $header) { + if (preg_match('{^HTTP/\S+ \d+}i', $header)) { + // In case of redirects, headers contain the headers of all responses + // so we can not return directly and need to keep iterating + $value = $header; + } + } + + return $value; + } + public function getHeaders() { return $this->headers; @@ -51,7 +68,7 @@ class Response } elseif (preg_match('{^HTTP/}i', $header)) { // TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary // - // In case of redirects, http_response_headers contains the headers of all responses + // In case of redirects, headers contains the headers of all responses // so we reset the flag when a new response is being parsed as we are only interested in the last response $value = null; } @@ -60,7 +77,6 @@ class Response return $value; } - public function getBody() { return $this->body; diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index d07823ec0..9cdc0c919 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -66,6 +66,7 @@ class HttpDownloader $this->options = array_replace_recursive($this->options, $options); $this->config = $config; + // TODO enable curl only on 5.6+ if older versions cause any problem if (extension_loaded('curl')) { $this->curl = new Http\CurlDownloader($io, $config, $options, $disableTls); } @@ -125,6 +126,11 @@ class HttpDownloader private function addJob($request, $sync = false) { + // capture username/password from URL if there is one + if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) { + $this->io->setAuthentication($originUrl, rawurldecode($match[1]), rawurldecode($match[2])); + } + $job = array( 'id' => $this->idGen++, 'status' => self::STATUS_QUEUED, @@ -138,7 +144,6 @@ class HttpDownloader $origin = $this->getOrigin($job['request']['url']); - // TODO experiment with allowing file:// through curl too if ($curl && preg_match('{^https?://}i', $job['request']['url'])) { $resolver = function ($resolve, $reject) use (&$job, $curl, $origin) { // start job @@ -183,10 +188,10 @@ class HttpDownloader $job['response'] = $response; // TODO look for more jobs to start once we throttle to max X jobs }, function ($e) use ($io, &$job) { - var_dump(__CLASS__ . __LINE__); - var_dump(gettype($e)); - var_dump($e->getMessage()); - die; + // var_dump(__CLASS__ . __LINE__); + // var_dump(get_class($e)); + // var_dump($e->getMessage()); + // die; $job['status'] = HttpDownloader::STATUS_FAILED; $job['exception'] = $e; }); @@ -248,9 +253,13 @@ class HttpDownloader private function getOrigin($url) { + if (0 === strpos($url, 'file://')) { + return $url; + } + $origin = parse_url($url, PHP_URL_HOST); - if ($origin === 'api.github.com') { + if (strpos($origin, '.github.com') === (strlen($origin) - 11)) { return 'github.com'; } @@ -258,6 +267,20 @@ class HttpDownloader return 'packagist.org'; } + // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl + // is the host without the path, so we look for the registered gitlab-domains matching the host here + if ( + is_array($this->config->get('gitlab-domains')) + && false === strpos($origin, '/') + && !in_array($origin, $this->config->get('gitlab-domains')) + ) { + foreach ($this->config->get('gitlab-domains') as $gitlabDomain) { + if (0 === strpos($gitlabDomain, $origin)) { + return $gitlabDomain; + } + } + } + return $origin ?: $url; } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 00fe35294..2709f7006 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -41,6 +41,7 @@ class RemoteFilesystem private $retryAuthFailure; private $lastHeaders; private $storeAuth; + private $authHelper; private $degradedMode = false; private $redirects; private $maxRedirects = 20; @@ -53,7 +54,7 @@ class RemoteFilesystem * @param array $options The options * @param bool $disableTls */ - public function __construct(IOInterface $io, Config $config = null, array $options = array(), $disableTls = false) + public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) { $this->io = $io; @@ -69,6 +70,7 @@ class RemoteFilesystem // handle the other externally set options normally. $this->options = array_replace_recursive($this->options, $options); $this->config = $config; + $this->authHelper = new AuthHelper($io, $config); } /** @@ -215,27 +217,6 @@ class RemoteFilesystem */ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true) { - if (strpos($originUrl, '.github.com') === (strlen($originUrl) - 11)) { - $originUrl = 'github.com'; - } - - // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl - // is the host without the path, so we look for the registered gitlab-domains matching the host here - if ( - $this->config - && is_array($this->config->get('gitlab-domains')) - && false === strpos($originUrl, '/') - && !in_array($originUrl, $this->config->get('gitlab-domains')) - ) { - foreach ($this->config->get('gitlab-domains') as $gitlabDomain) { - if (0 === strpos($gitlabDomain, $originUrl)) { - $originUrl = $gitlabDomain; - break; - } - } - unset($gitlabDomain); - } - $this->scheme = parse_url($fileUrl, PHP_URL_SCHEME); $this->bytesMax = 0; $this->originUrl = $originUrl; @@ -247,11 +228,6 @@ class RemoteFilesystem $this->lastHeaders = array(); $this->redirects = 1; // The first request counts. - // capture username/password from URL if there is one - if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $fileUrl, $match)) { - $this->io->setAuthentication($originUrl, rawurldecode($match[1]), rawurldecode($match[2])); - } - $tempAdditionalOptions = $additionalOptions; if (isset($tempAdditionalOptions['retry-auth-failure'])) { $this->retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure']; @@ -272,14 +248,6 @@ class RemoteFilesystem $origFileUrl = $fileUrl; - if (isset($options['github-token'])) { - // only add the access_token if it is actually a github URL (in case we were redirected to S3) - if (preg_match('{^https?://([a-z0-9-]+\.)*github\.com/}', $fileUrl)) { - $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token']; - } - unset($options['github-token']); - } - if (isset($options['gitlab-token'])) { $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token']; unset($options['gitlab-token']); @@ -400,7 +368,7 @@ class RemoteFilesystem // check for bitbucket login page asking to authenticate if ($originUrl === 'bitbucket.org' - && !$this->isPublicBitBucketDownload($fileUrl) + && !$this->authHelper->isPublicBitBucketDownload($fileUrl) && substr($fileUrl, -4) === '.zip' && (!$locationHeader || substr($locationHeader, -4) !== '.zip') && $contentType && preg_match('{^text/html\b}i', $contentType) @@ -544,8 +512,7 @@ class RemoteFilesystem $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); if ($this->storeAuth && $this->config) { - $authHelper = new AuthHelper($this->io, $this->config); - $authHelper->storeAuth($this->originUrl, $this->storeAuth); + $this->authHelper->storeAuth($this->originUrl, $this->storeAuth); $this->storeAuth = false; } @@ -650,111 +617,14 @@ class RemoteFilesystem protected function promptAuthAndRetry($httpStatus, $reason = null, $warning = null, $headers = array()) { - if ($this->config && in_array($this->originUrl, $this->config->get('github-domains'), true)) { - $gitHubUtil = new GitHub($this->io, $this->config, null); - $message = "\n"; + $result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $warning, $headers); - $rateLimited = $gitHubUtil->isRateLimited($headers); - if ($rateLimited) { - $rateLimit = $gitHubUtil->getRateLimit($headers); - if ($this->io->hasAuthentication($this->originUrl)) { - $message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.'; - } else { - $message = 'Create a GitHub OAuth token to go over the API rate limit.'; - } + $this->storeAuth = $result['storeAuth']; + $this->retry = $result['retry']; - $message = sprintf( - 'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$this->fileUrl.'. '.$message.' You can also wait until %s for the rate limit to reset.', - $rateLimit['limit'], - $rateLimit['reset'] - )."\n"; - } else { - $message .= 'Could not fetch '.$this->fileUrl.', please '; - if ($this->io->hasAuthentication($this->originUrl)) { - $message .= 'review your configured GitHub OAuth token or enter a new one to access private repos'; - } else { - $message .= 'create a GitHub OAuth token to access private repos'; - } - } - - if (!$gitHubUtil->authorizeOAuth($this->originUrl) - && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($this->originUrl, $message)) - ) { - throw new TransportException('Could not authenticate against '.$this->originUrl, 401); - } - } elseif ($this->config && in_array($this->originUrl, $this->config->get('gitlab-domains'), true)) { - $message = "\n".'Could not fetch '.$this->fileUrl.', enter your ' . $this->originUrl . ' credentials ' .($httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit'); - $gitLabUtil = new GitLab($this->io, $this->config, null); - - if ($this->io->hasAuthentication($this->originUrl) && ($auth = $this->io->getAuthentication($this->originUrl)) && $auth['password'] === 'private-token') { - throw new TransportException("Invalid credentials for '" . $this->fileUrl . "', aborting.", $httpStatus); - } - - if (!$gitLabUtil->authorizeOAuth($this->originUrl) - && (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, $message)) - ) { - throw new TransportException('Could not authenticate against '.$this->originUrl, 401); - } - } elseif ($this->config && $this->originUrl === 'bitbucket.org') { - $askForOAuthToken = true; - if ($this->io->hasAuthentication($this->originUrl)) { - $auth = $this->io->getAuthentication($this->originUrl); - if ($auth['username'] !== 'x-token-auth') { - $bitbucketUtil = new Bitbucket($this->io, $this->config); - $accessToken = $bitbucketUtil->requestToken($this->originUrl, $auth['username'], $auth['password']); - if (!empty($accessToken)) { - $this->io->setAuthentication($this->originUrl, 'x-token-auth', $accessToken); - $askForOAuthToken = false; - } - } else { - throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); - } - } - - if ($askForOAuthToken) { - $message = "\n".'Could not fetch ' . $this->fileUrl . ', please create a bitbucket OAuth token to ' . (($httpStatus === 401 || $httpStatus === 403) ? 'access private repos' : 'go over the API rate limit'); - $bitBucketUtil = new Bitbucket($this->io, $this->config); - if (! $bitBucketUtil->authorizeOAuth($this->originUrl) - && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($this->originUrl, $message)) - ) { - throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); - } - } - } else { - // 404s are only handled for github - if ($httpStatus === 404) { - return; - } - - // fail if the console is not interactive - if (!$this->io->isInteractive()) { - if ($httpStatus === 401) { - $message = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console to authenticate"; - } - if ($httpStatus === 403) { - $message = "The '" . $this->fileUrl . "' URL could not be accessed: " . $reason; - } - - throw new TransportException($message, $httpStatus); - } - // fail if we already have auth - if ($this->io->hasAuthentication($this->originUrl)) { - throw new TransportException("Invalid credentials for '" . $this->fileUrl . "', aborting.", $httpStatus); - } - - $this->io->overwriteError(''); - if ($warning) { - $this->io->writeError(' '.$warning.''); - } - $this->io->writeError(' Authentication required ('.parse_url($this->fileUrl, PHP_URL_HOST).'):'); - $username = $this->io->ask(' Username: '); - $password = $this->io->askAndHideAnswer(' Password: '); - $this->io->setAuthentication($this->originUrl, $username, $password); - $this->storeAuth = $this->config->get('store-auths'); + if ($this->retry) { + throw new TransportException('RETRY'); } - - $this->retry = true; - throw new TransportException('RETRY'); } protected function getOptionsForUrl($originUrl, $additionalOptions) @@ -814,27 +684,7 @@ class RemoteFilesystem $headers[] = 'Connection: close'; } - if ($this->io->hasAuthentication($originUrl)) { - $auth = $this->io->getAuthentication($originUrl); - if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) { - $options['github-token'] = $auth['username']; - } elseif ($this->config && in_array($originUrl, $this->config->get('gitlab-domains'), true)) { - if ($auth['password'] === 'oauth2') { - $headers[] = 'Authorization: Bearer '.$auth['username']; - } elseif ($auth['password'] === 'private-token') { - $headers[] = 'PRIVATE-TOKEN: '.$auth['username']; - } - } elseif ('bitbucket.org' === $originUrl - && $this->fileUrl !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL && 'x-token-auth' === $auth['username'] - ) { - if (!$this->isPublicBitBucketDownload($this->fileUrl)) { - $headers[] = 'Authorization: Bearer ' . $auth['password']; - } - } else { - $authStr = base64_encode($auth['username'] . ':' . $auth['password']); - $headers[] = 'Authorization: Basic '.$authStr; - } - } + $headers = $this->authHelper->addAuthenticationHeader($headers, $originUrl, $this->fileUrl); $options['http']['follow_location'] = 0; @@ -961,29 +811,4 @@ class RemoteFilesystem return parse_url($url, PHP_URL_HOST).':'.$port; } - - /** - * @link https://github.com/composer/composer/issues/5584 - * - * @param string $urlToBitBucketFile URL to a file at bitbucket.org. - * - * @return bool Whether the given URL is a public BitBucket download which requires no authentication. - */ - private function isPublicBitBucketDownload($urlToBitBucketFile) - { - $domain = parse_url($urlToBitBucketFile, PHP_URL_HOST); - if (strpos($domain, 'bitbucket.org') === false) { - // Bitbucket downloads are hosted on amazonaws. - // We do not need to authenticate there at all - return true; - } - - $path = parse_url($urlToBitBucketFile, PHP_URL_PATH); - - // Path for a public download follows this pattern /{user}/{repo}/downloads/{whatever} - // {@link https://blog.bitbucket.org/2009/04/12/new-feature-downloads/} - $pathParts = explode('/', $path); - - return count($pathParts) >= 4 && $pathParts[3] == 'downloads'; - } } diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index b25b307a1..a87bc6d8b 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -41,6 +41,32 @@ final class StreamContextFactory 'max_redirects' => 20, )); + $options = array_replace_recursive($options, self::initOptions($url, $defaultOptions)); + unset($defaultOptions['http']['header']); + $options = array_replace_recursive($options, $defaultOptions); + + if (isset($options['http']['header'])) { + $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); + } + + return stream_context_create($options, $defaultParams); + } + + /** + * @param string $url + * @param array $options + * @return array ['http' => ['header' => [...], 'proxy' => '..', 'request_fulluri' => bool]] formatted as a stream context array + */ + public static function initOptions($url, array $options) + { + // Make sure the headers are in an array form + if (!isset($options['http']['header'])) { + $options['http']['header'] = array(); + } + if (is_string($options['http']['header'])) { + $options['http']['header'] = explode("\r\n", $options['http']['header']); + } + // Handle HTTP_PROXY/http_proxy on CLI only for security reasons if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); @@ -117,42 +143,36 @@ final class StreamContextFactory } $auth = base64_encode($auth); - // Preserve headers if already set in default options - if (isset($defaultOptions['http']['header'])) { - if (is_string($defaultOptions['http']['header'])) { - $defaultOptions['http']['header'] = array($defaultOptions['http']['header']); - } - $defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; - } else { - $options['http']['header'] = array("Proxy-Authorization: Basic {$auth}"); - } + $options['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; } } - $options = array_replace_recursive($options, $defaultOptions); - - if (isset($options['http']['header'])) { - $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); - } - if (defined('HHVM_VERSION')) { $phpVersion = 'HHVM ' . HHVM_VERSION; } else { $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; } + if (extension_loaded('curl')) { + $curl = curl_version(); + $httpVersion = 'curl '.$curl['version']; + } else { + $httpVersion = 'streams'; + } + if (!isset($options['http']['header']) || false === stripos(implode('', $options['http']['header']), 'user-agent')) { $options['http']['header'][] = sprintf( - 'User-Agent: Composer/%s (%s; %s; %s%s)', - Composer::VERSION === '@package_version@' ? 'source' : Composer::VERSION, + 'User-Agent: Composer/%s (%s; %s; %s; %s%s)', + Composer::VERSION === '@package_version@' ? Composer::SOURCE_VERSION : Composer::VERSION, function_exists('php_uname') ? php_uname('s') : 'Unknown', function_exists('php_uname') ? php_uname('r') : 'Unknown', $phpVersion, + $httpVersion, getenv('CI') ? '; CI' : '' ); } - return stream_context_create($options, $defaultParams); + return $options; } /** diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index 7da88bc8a..8d1bf3194 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -17,6 +17,20 @@ use PHPUnit\Framework\TestCase; class RemoteFilesystemTest extends TestCase { + private function getConfigMock() + { + $config = $this->getMockBuilder('Composer\Config')->getMock(); + $config->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($key) { + if ($key === 'github-domains' || $key === 'gitlab-domains') { + return array(); + } + })); + + return $config; + } + public function testGetOptionsForUrl() { $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); @@ -101,7 +115,7 @@ class RemoteFilesystemTest extends TestCase public function testCallbackGetFileSize() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock()); + $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $this->callCallbackGet($fs, STREAM_NOTIFY_FILE_SIZE_IS, 0, '', 0, 0, 20); $this->assertAttributeEquals(20, 'bytesMax', $fs); } @@ -114,7 +128,7 @@ class RemoteFilesystemTest extends TestCase ->method('overwriteError') ; - $fs = new RemoteFilesystem($io); + $fs = new RemoteFilesystem($io, $this->getConfigMock()); $this->setAttribute($fs, 'bytesMax', 20); $this->setAttribute($fs, 'progress', true); @@ -124,7 +138,7 @@ class RemoteFilesystemTest extends TestCase public function testCallbackGetPassesThrough404() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock()); + $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0)); } @@ -139,7 +153,7 @@ class RemoteFilesystemTest extends TestCase ->method('setAuthentication') ->with($this->equalTo('github.com'), $this->equalTo('user'), $this->equalTo('pass')); - $fs = new RemoteFilesystem($io); + $fs = new RemoteFilesystem($io, $this->getConfigMock()); try { $fs->getContents('github.com', 'https://user:pass@github.com/composer/composer/404'); } catch (\Exception $e) { @@ -150,14 +164,14 @@ class RemoteFilesystemTest extends TestCase public function testGetContents() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock()); + $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $this->assertContains('testGetContents', $fs->getContents('http://example.org', 'file://'.__FILE__)); } public function testCopy() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock()); + $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); $file = tempnam(sys_get_temp_dir(), 'c'); $this->assertTrue($fs->copy('http://example.org', 'file://'.__FILE__, $file)); @@ -218,7 +232,7 @@ class RemoteFilesystemTest extends TestCase ->disableOriginalConstructor() ->getMock(); - $rfs = new RemoteFilesystem($io); + $rfs = new RemoteFilesystem($io, $this->getConfigMock()); $hostname = parse_url($url, PHP_URL_HOST); $result = $rfs->getContents($hostname, $url, false); @@ -240,14 +254,6 @@ class RemoteFilesystemTest extends TestCase ->disableOriginalConstructor() ->getMock(); - $config = $this - ->getMockBuilder('Composer\Config') - ->getMock(); - $config - ->method('get') - ->withAnyParameters() - ->willReturn(array()); - $domains = array(); $io ->expects($this->any()) @@ -267,7 +273,7 @@ class RemoteFilesystemTest extends TestCase 'password' => '1A0yeK5Po3ZEeiiRiMWLivS0jirLdoGuaSGq9NvESFx1Fsdn493wUDXC8rz_1iKVRTl1GINHEUCsDxGh5lZ=', )); - $rfs = new RemoteFilesystem($io, $config); + $rfs = new RemoteFilesystem($io, $this->getConfigMock()); $hostname = parse_url($url, PHP_URL_HOST); $result = $rfs->getContents($hostname, $url, false); @@ -278,7 +284,7 @@ class RemoteFilesystemTest extends TestCase protected function callGetOptionsForUrl($io, array $args = array(), array $options = array(), $fileUrl = '') { - $fs = new RemoteFilesystem($io, null, $options); + $fs = new RemoteFilesystem($io, $this->getConfigMock(), $options); $ref = new \ReflectionMethod($fs, 'getOptionsForUrl'); $prop = new \ReflectionProperty($fs, 'fileUrl'); $ref->setAccessible(true); From 9986b797fb1b9acca9add379337717fe16a27ea0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 16 Nov 2018 14:28:00 +0100 Subject: [PATCH 034/321] Add support for redirects/retries in curl downloader --- src/Composer/Util/Http/CurlDownloader.php | 72 +++++++++++++++++++---- src/Composer/Util/HttpDownloader.php | 35 +---------- src/Composer/Util/Url.php | 34 +++++++++++ 3 files changed, 96 insertions(+), 45 deletions(-) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 2accb7a0c..83f07c44a 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -19,6 +19,7 @@ use Composer\CaBundle\CaBundle; use Composer\Util\RemoteFilesystem; use Composer\Util\StreamContextFactory; use Composer\Util\AuthHelper; +use Composer\Util\Url; use Psr\Log\LoggerInterface; use React\Promise\Promise; @@ -132,11 +133,10 @@ class CurlDownloader } curl_setopt($curlHandle, CURLOPT_URL, $url); - curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($curlHandle, CURLOPT_MAXREDIRS, 20); + curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false); //curl_setopt($curlHandle, CURLOPT_DNS_USE_GLOBAL_CACHE, false); curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($curlHandle, CURLOPT_TIMEOUT, 10); // TODO increase + curl_setopt($curlHandle, CURLOPT_TIMEOUT, 60); curl_setopt($curlHandle, CURLOPT_WRITEHEADER, $headerHandle); curl_setopt($curlHandle, CURLOPT_FILE, $bodyHandle); curl_setopt($curlHandle, CURLOPT_ENCODING, "gzip"); @@ -181,7 +181,6 @@ class CurlDownloader 'options' => $originalOptions, 'progress' => $progress, 'curlHandle' => $curlHandle, - //'callback' => $params['notification'], 'filename' => $copyTo, 'headerHandle' => $headerHandle, 'bodyHandle' => $bodyHandle, @@ -252,12 +251,24 @@ class CurlDownloader $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); } - $response = $this->retryIfAuthNeeded($job, $response); + $result = $this->isAuthenticatedRetryNeeded($job, $response); + if ($result['retry']) { + if ($job['filename']) { + @unlink($job['filename'].'~'); + } + + $this->restartJob($job, $job['url'], array('storeAuth' => $result['storeAuth'])); + continue; + } // handle 3xx redirects, 304 Not Modified is excluded if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['redirects'] < $this->maxRedirects) { // TODO - $response = $this->handleRedirect($job, $response); + $location = $this->handleRedirect($job, $response); + if ($location) { + $this->restartJob($job, $location, array('redirects' => $job['attributes']['redirects'] + 1)); + continue; + } } // fail 4xx and 5xx responses and capture the response @@ -331,7 +342,39 @@ class CurlDownloader } } - private function retryIfAuthNeeded(array $job, Response $response) + private function handleRedirect(array $job, Response $response) + { + if ($locationHeader = $response->getHeader('location')) { + if (parse_url($locationHeader, PHP_URL_SCHEME)) { + // Absolute URL; e.g. https://example.com/composer + $targetUrl = $locationHeader; + } elseif (parse_url($locationHeader, PHP_URL_HOST)) { + // Scheme relative; e.g. //example.com/foo + $targetUrl = parse_url($job['url'], PHP_URL_SCHEME).':'.$locationHeader; + } elseif ('/' === $locationHeader[0]) { + // Absolute path; e.g. /foo + $urlHost = parse_url($job['url'], PHP_URL_HOST); + + // Replace path using hostname as an anchor. + $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $job['url']); + } else { + // Relative path; e.g. foo + // This actually differs from PHP which seems to add duplicate slashes. + $targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $job['url']); + } + } + + if (!empty($targetUrl)) { + $this->io->writeError('', true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $job['redirects'] + 1, $targetUrl), true, IOInterface::DEBUG); + + return $targetUrl; + } + + throw new TransportException('The "'.$job['url'].'" file could not be downloaded, got redirect without Location ('.$response->getStatusMessage().')'); + } + + private function isAuthenticatedRetryNeeded(array $job, Response $response) { if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) { $warning = null; @@ -345,7 +388,7 @@ class CurlDownloader $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $warning, $response->getHeaders()); if ($result['retry']) { - // TODO retry somehow using $result['storeAuth'] in the attributes + return $result; } } @@ -376,15 +419,22 @@ class CurlDownloader if ($job['attributes']['retryAuthFailure']) { $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401); if ($result['retry']) { - // TODO ... - // TODO return early here to abort failResponse + return $result; } } throw $this->failResponse($job, $response, $needsAuthRetry); } - return $response; + return array('retry' => false, 'storeAuth' => false); + } + + private function restartJob(array $job, $url, array $attributes = array()) + { + $attributes = array_merge($job['attributes'], $attributes); + $origin = Url::getOrigin($this->config, $url); + + $this->initDownload($job['resolve'], $job['reject'], $origin, $url, $job['originalOptions'], $job['filename'], $attributes); } private function failResponse(array $job, Response $response, $errorMessage) diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 9cdc0c919..da3906728 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -142,7 +142,7 @@ class HttpDownloader $rfs = $this->rfs; $io = $this->io; - $origin = $this->getOrigin($job['request']['url']); + $origin = Url::getOrigin($this->config, $job['request']['url']); if ($curl && preg_match('{^https?://}i', $job['request']['url'])) { $resolver = function ($resolve, $reject) use (&$job, $curl, $origin) { @@ -250,37 +250,4 @@ class HttpDownloader return $resp; } - - private function getOrigin($url) - { - if (0 === strpos($url, 'file://')) { - return $url; - } - - $origin = parse_url($url, PHP_URL_HOST); - - if (strpos($origin, '.github.com') === (strlen($origin) - 11)) { - return 'github.com'; - } - - if ($origin === 'repo.packagist.org') { - return 'packagist.org'; - } - - // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl - // is the host without the path, so we look for the registered gitlab-domains matching the host here - if ( - is_array($this->config->get('gitlab-domains')) - && false === strpos($origin, '/') - && !in_array($origin, $this->config->get('gitlab-domains')) - ) { - foreach ($this->config->get('gitlab-domains') as $gitlabDomain) { - if (0 === strpos($gitlabDomain, $origin)) { - return $gitlabDomain; - } - } - } - - return $origin ?: $url; - } } diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php index 4a5d5f90c..3266c4220 100644 --- a/src/Composer/Util/Url.php +++ b/src/Composer/Util/Url.php @@ -52,4 +52,38 @@ class Url return $url; } + + public static function getOrigin(Config $config, $url) + { + if (0 === strpos($url, 'file://')) { + return $url; + } + + $origin = parse_url($url, PHP_URL_HOST); + + if (strpos($origin, '.github.com') === (strlen($origin) - 11)) { + return 'github.com'; + } + + if ($origin === 'repo.packagist.org') { + return 'packagist.org'; + } + + // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl + // is the host without the path, so we look for the registered gitlab-domains matching the host here + if ( + is_array($config->get('gitlab-domains')) + && false === strpos($origin, '/') + && !in_array($origin, $config->get('gitlab-domains')) + ) { + foreach ($config->get('gitlab-domains') as $gitlabDomain) { + if (0 === strpos($gitlabDomain, $origin)) { + return $gitlabDomain; + } + } + } + + return $origin ?: $url; + } + } From 64384f8b15314e618f1a6821dd71325e8b8178ab Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 16 Nov 2018 14:43:25 +0100 Subject: [PATCH 035/321] Fix tests --- src/Composer/Util/HttpDownloader.php | 14 ++--- src/Composer/Util/Url.php | 8 ++- .../Composer/Test/Util/HttpDownloaderTest.php | 51 +++++++++++++++++++ .../Test/Util/RemoteFilesystemTest.php | 19 ------- 4 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 tests/Composer/Test/Util/HttpDownloaderTest.php diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index da3906728..b259ef3e5 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -126,11 +126,6 @@ class HttpDownloader private function addJob($request, $sync = false) { - // capture username/password from URL if there is one - if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) { - $this->io->setAuthentication($originUrl, rawurldecode($match[1]), rawurldecode($match[2])); - } - $job = array( 'id' => $this->idGen++, 'status' => self::STATUS_QUEUED, @@ -138,12 +133,17 @@ class HttpDownloader 'sync' => $sync, ); + $origin = Url::getOrigin($this->config, $job['request']['url']); + + // capture username/password from URL if there is one + if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) { + $this->io->setAuthentication($origin, rawurldecode($match[1]), rawurldecode($match[2])); + } + $curl = $this->curl; $rfs = $this->rfs; $io = $this->io; - $origin = Url::getOrigin($this->config, $job['request']['url']); - if ($curl && preg_match('{^https?://}i', $job['request']['url'])) { $resolver = function ($resolve, $reject) use (&$job, $curl, $origin) { // start job diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php index 3266c4220..2874524ed 100644 --- a/src/Composer/Util/Url.php +++ b/src/Composer/Util/Url.php @@ -59,7 +59,7 @@ class Url return $url; } - $origin = parse_url($url, PHP_URL_HOST); + $origin = (string) parse_url($url, PHP_URL_HOST); if (strpos($origin, '.github.com') === (strlen($origin) - 11)) { return 'github.com'; @@ -69,6 +69,10 @@ class Url return 'packagist.org'; } + if ($origin === '') { + $origin = $url; + } + // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl // is the host without the path, so we look for the registered gitlab-domains matching the host here if ( @@ -83,7 +87,7 @@ class Url } } - return $origin ?: $url; + return $origin; } } diff --git a/tests/Composer/Test/Util/HttpDownloaderTest.php b/tests/Composer/Test/Util/HttpDownloaderTest.php new file mode 100644 index 000000000..b65aa760a --- /dev/null +++ b/tests/Composer/Test/Util/HttpDownloaderTest.php @@ -0,0 +1,51 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\HttpDownloader; +use PHPUnit\Framework\TestCase; + +class HttpDownloaderTest extends TestCase +{ + private function getConfigMock() + { + $config = $this->getMockBuilder('Composer\Config')->getMock(); + $config->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($key) { + if ($key === 'github-domains' || $key === 'gitlab-domains') { + return array(); + } + })); + + return $config; + } + + /** + * @group slow + */ + public function testCaptureAuthenticationParamsFromUrl() + { + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io->expects($this->once()) + ->method('setAuthentication') + ->with($this->equalTo('github.com'), $this->equalTo('user'), $this->equalTo('pass')); + + $fs = new HttpDownloader($io, $this->getConfigMock()); + try { + $fs->get('https://user:pass@github.com/composer/composer/404'); + } catch (\Composer\Downloader\TransportException $e) { + $this->assertNotEquals(200, $e->getCode()); + } + } +} diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index 8d1bf3194..2c7f3112a 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -143,25 +143,6 @@ class RemoteFilesystemTest extends TestCase $this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0)); } - /** - * @group slow - */ - public function testCaptureAuthenticationParamsFromUrl() - { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $io->expects($this->once()) - ->method('setAuthentication') - ->with($this->equalTo('github.com'), $this->equalTo('user'), $this->equalTo('pass')); - - $fs = new RemoteFilesystem($io, $this->getConfigMock()); - try { - $fs->getContents('github.com', 'https://user:pass@github.com/composer/composer/404'); - } catch (\Exception $e) { - $this->assertInstanceOf('Composer\Downloader\TransportException', $e); - $this->assertNotEquals(200, $e->getCode()); - } - } - public function testGetContents() { $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); From 5d2b3276ebf6b98114eb73a5bd315e59f0f08c0a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 16 Nov 2018 15:38:01 +0100 Subject: [PATCH 036/321] Avoid starting all jobs immediately --- src/Composer/Util/Http/CurlDownloader.php | 36 +++---- src/Composer/Util/HttpDownloader.php | 110 +++++++++++++++------- 2 files changed, 92 insertions(+), 54 deletions(-) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 83f07c44a..4015b0bac 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -95,12 +95,17 @@ class CurlDownloader public function download($resolve, $reject, $origin, $url, $options, $copyTo = null) { - return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo); + $attributes = array(); + if (isset($options['retry-auth-failure'])) { + $attributes['retryAuthFailure'] = $options['retry-auth-failure']; + unset($options['retry-auth-failure']); + } + + return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo, $attributes); } private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array()) { - // TODO allow setting attributes somehow $attributes = array_merge(array( 'retryAuthFailure' => true, 'redirects' => 1, @@ -193,12 +198,12 @@ class CurlDownloader $this->io->writeError('Downloading ' . $url . $usingProxy . $ifModified, true, IOInterface::DEBUG); $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); +// TODO progress //$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false); } public function tick() { - // TODO check we have active handles before doing this if (!$this->jobs) { return; } @@ -229,6 +234,7 @@ class CurlDownloader $statusCode = null; $response = null; try { +// TODO progress //$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']); if (CURLE_OK !== $errno) { throw new TransportException($error); @@ -263,7 +269,6 @@ class CurlDownloader // handle 3xx redirects, 304 Not Modified is excluded if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['redirects'] < $this->maxRedirects) { - // TODO $location = $this->handleRedirect($job, $response); if ($location) { $this->restartJob($job, $location, array('redirects' => $job['attributes']['redirects'] + 1)); @@ -274,6 +279,7 @@ class CurlDownloader // fail 4xx and 5xx responses and capture the response if ($statusCode >= 400 && $statusCode <= 599) { throw $this->failResponse($job, $response, $response->getStatusMessage()); +// TODO progress // $this->io->overwriteError("Downloading (failed)", false); } @@ -320,24 +326,13 @@ class CurlDownloader if ($this->jobs[$i]['progress'] !== $progress) { $previousProgress = $this->jobs[$i]['progress']; $this->jobs[$i]['progress'] = $progress; - try { - //$this->onProgress($curlHandle, $this->jobs[$i]['callback'], $progress, $previousProgress); - } catch (TransportException $e) { - var_dump('Caught '.$e->getMessage());die; - unset($this->jobs[$i]); - curl_multi_remove_handle($this->multiHandle, $curlHandle); - curl_close($curlHandle); - fclose($job['headerHandle']); - fclose($job['bodyHandle']); - if ($job['filename']) { - @unlink($job['filename'].'~'); - } - call_user_func($job['reject'], $e); - } + // TODO + //$this->onProgress($curlHandle, $this->jobs[$i]['callback'], $progress, $previousProgress); } } } catch (\Exception $e) { + // TODO var_dump('Caught2', get_class($e), $e->getMessage(), $e);die; } } @@ -444,13 +439,10 @@ class CurlDownloader private function onProgress($curlHandle, callable $notify, array $progress, array $previousProgress) { + // TODO add support for progress if (300 <= $progress['http_code'] && $progress['http_code'] < 400) { return; } - if (!$previousProgress['http_code'] && $progress['http_code'] && $progress['http_code'] < 200 || 400 <= $progress['http_code']) { - $code = 403 === $progress['http_code'] ? STREAM_NOTIFY_AUTH_RESULT : STREAM_NOTIFY_FAILURE; - $notify($code, STREAM_NOTIFY_SEVERITY_ERR, curl_error($curlHandle), $progress['http_code'], 0, 0, false); - } if ($previousProgress['download_content_length'] < $progress['download_content_length']) { $notify(STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['download_content_length'], false); } diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index b259ef3e5..0e882c4a3 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -33,8 +33,8 @@ class HttpDownloader private $config; private $jobs = array(); private $options = array(); - private $index; - private $progress; + private $runningJobs = 0; + private $maxJobs = 10; private $lastProgress; private $disableTls = false; private $curl; @@ -42,8 +42,6 @@ class HttpDownloader private $idGen = 0; /** - * Constructor. - * * @param IOInterface $io The IO instance * @param Config $config The config * @param array $options The options @@ -131,35 +129,24 @@ class HttpDownloader 'status' => self::STATUS_QUEUED, 'request' => $request, 'sync' => $sync, + 'origin' => Url::getOrigin($this->config, $request['url']), ); - $origin = Url::getOrigin($this->config, $job['request']['url']); - // capture username/password from URL if there is one if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) { - $this->io->setAuthentication($origin, rawurldecode($match[1]), rawurldecode($match[2])); + $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2])); } - $curl = $this->curl; $rfs = $this->rfs; - $io = $this->io; - if ($curl && preg_match('{^https?://}i', $job['request']['url'])) { - $resolver = function ($resolve, $reject) use (&$job, $curl, $origin) { - // start job - $url = $job['request']['url']; - $options = $job['request']['options']; - - $job['status'] = HttpDownloader::STATUS_STARTED; - - if ($job['request']['copyTo']) { - $curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); - } else { - $curl->download($resolve, $reject, $origin, $url, $options); - } + if ($this->curl && preg_match('{^https?://}i', $job['request']['url'])) { + $resolver = function ($resolve, $reject) use (&$job) { + $job['status'] = HttpDownloader::STATUS_QUEUED; + $job['resolve'] = $resolve; + $job['reject'] = $reject; }; } else { - $resolver = function ($resolve, $reject) use (&$job, $rfs, $curl, $origin) { + $resolver = function ($resolve, $reject) use (&$job, $rfs) { // start job $url = $job['request']['url']; $options = $job['request']['options']; @@ -167,11 +154,11 @@ class HttpDownloader $job['status'] = HttpDownloader::STATUS_STARTED; if ($job['request']['copyTo']) { - $result = $rfs->copy($origin, $url, $job['request']['copyTo'], false /* TODO progress */, $options); + $result = $rfs->copy($job['origin'], $url, $job['request']['copyTo'], false /* TODO progress */, $options); $resolve($result); } else { - $body = $rfs->getContents($origin, $url, false /* TODO progress */, $options); + $body = $rfs->getContents($job['origin'], $url, false /* TODO progress */, $options); $headers = $rfs->getLastHeaders(); $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $body); @@ -180,26 +167,85 @@ class HttpDownloader }; } + $downloader = $this; + $io = $this->io; + $canceler = function () {}; $promise = new Promise($resolver, $canceler); - $promise->then(function ($response) use (&$job) { + $promise->then(function ($response) use (&$job, $downloader) { $job['status'] = HttpDownloader::STATUS_COMPLETED; $job['response'] = $response; - // TODO look for more jobs to start once we throttle to max X jobs - }, function ($e) use ($io, &$job) { - // var_dump(__CLASS__ . __LINE__); - // var_dump(get_class($e)); - // var_dump($e->getMessage()); - // die; + + // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped + $downloader->markJobDone(); + $downloader->scheduleNextJob(); + + return $response; + }, function ($e) use ($io, &$job, $downloader) { $job['status'] = HttpDownloader::STATUS_FAILED; $job['exception'] = $e; + + $downloader->markJobDone(); + + throw $e; }); $this->jobs[$job['id']] =& $job; + if ($this->runningJobs < $this->maxJobs) { + $this->startJob($job['id']); + } + return array($job, $promise); } + private function startJob($id) + { + $job =& $this->jobs[$id]; + if ($job['status'] !== self::STATUS_QUEUED) { + return; + } + + // start job + $job['status'] = self::STATUS_STARTED; + $this->runningJobs++; + + $resolve = $job['resolve']; + $reject = $job['reject']; + $url = $job['request']['url']; + $options = $job['request']['options']; + $origin = $job['origin']; + + if ($job['request']['copyTo']) { + $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); + } else { + $this->curl->download($resolve, $reject, $origin, $url, $options); + } + } + + /** + * @private + */ + public function markJobDone() + { + $this->runningJobs--; + } + + /** + * @private + */ + public function scheduleNextJob() + { + foreach ($this->jobs as $job) { + if ($job['status'] === self::STATUS_QUEUED) { + $this->startJob($job['id']); + if ($this->runningJobs >= $this->maxJobs) { + return; + } + } + } + } + public function wait($index = null, $progress = false) { while (true) { From 788a822b240dfc51569ebe842079a8a05f274ff9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 16 Nov 2018 17:35:06 +0100 Subject: [PATCH 037/321] Add some phpdocs --- .../Repository/PlatformRepository.php | 4 ++ src/Composer/Util/AuthHelper.php | 47 +++++++++++++------ src/Composer/Util/Url.php | 11 ++++- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 6d6e04d2f..f4c38f3ea 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -308,6 +308,10 @@ class PlatformRepository extends ArrayRepository $this->addPackage($ext); } + /** + * @param string $name + * @return string + */ private function buildPackageName($name) { return 'ext-' . str_replace(' ', '-', $name); diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index e80a5b0c3..3679b93da 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -30,7 +30,11 @@ class AuthHelper $this->config = $config; } - public function storeAuth($originUrl, $storeAuth) + /** + * @param string $origin + * @param string|bool $storeAuth + */ + public function storeAuth($origin, $storeAuth) { $store = false; $configSource = $this->config->getAuthConfigSource(); @@ -38,7 +42,7 @@ class AuthHelper $store = $configSource; } elseif ($storeAuth === 'prompt') { $answer = $this->io->askAndValidate( - 'Do you want to store credentials for '.$originUrl.' in '.$configSource->getName().' ? [Yn] ', + 'Do you want to store credentials for '.$origin.' in '.$configSource->getName().' ? [Yn] ', function ($value) { $input = strtolower(substr(trim($value), 0, 1)); if (in_array($input, array('y','n'))) { @@ -56,14 +60,23 @@ class AuthHelper } if ($store) { $store->addConfigSetting( - 'http-basic.'.$originUrl, - $this->io->getAuthentication($originUrl) + 'http-basic.'.$origin, + $this->io->getAuthentication($origin) ); } } - - public function promptAuthIfNeeded($url, $origin, $httpStatus, $reason = null, $warning = null, $headers = array()) + /** + * @param string $url + * @param string $origin + * @param int $statusCode HTTP status code that triggered this call + * @param string|null $reason a message/description explaining why this was called + * @param string $warning an authentication warning returned by the server as {"warning": ".."}, if present + * @param string[] $headers + * @return array containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be + * retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json + */ + public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $warning = null, $headers = array()) { $storeAuth = false; $retry = false; @@ -101,11 +114,11 @@ class AuthHelper throw new TransportException('Could not authenticate against '.$origin, 401); } } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) { - $message = "\n".'Could not fetch '.$url.', enter your ' . $origin . ' credentials ' .($httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit'); + $message = "\n".'Could not fetch '.$url.', enter your ' . $origin . ' credentials ' .($statusCode === 401 ? 'to access private repos' : 'to go over the API rate limit'); $gitLabUtil = new GitLab($this->io, $this->config, null); if ($this->io->hasAuthentication($origin) && ($auth = $this->io->getAuthentication($origin)) && $auth['password'] === 'private-token') { - throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $httpStatus); + throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); } if (!$gitLabUtil->authorizeOAuth($origin) @@ -130,7 +143,7 @@ class AuthHelper } if ($askForOAuthToken) { - $message = "\n".'Could not fetch ' . $url . ', please create a bitbucket OAuth token to ' . (($httpStatus === 401 || $httpStatus === 403) ? 'access private repos' : 'go over the API rate limit'); + $message = "\n".'Could not fetch ' . $url . ', please create a bitbucket OAuth token to ' . (($statusCode === 401 || $statusCode === 403) ? 'access private repos' : 'go over the API rate limit'); $bitBucketUtil = new Bitbucket($this->io, $this->config); if (! $bitBucketUtil->authorizeOAuth($origin) && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($origin, $message)) @@ -140,24 +153,24 @@ class AuthHelper } } else { // 404s are only handled for github - if ($httpStatus === 404) { + if ($statusCode === 404) { return; } // fail if the console is not interactive if (!$this->io->isInteractive()) { - if ($httpStatus === 401) { + if ($statusCode === 401) { $message = "The '" . $url . "' URL required authentication.\nYou must be using the interactive console to authenticate"; } - if ($httpStatus === 403) { + if ($statusCode === 403) { $message = "The '" . $url . "' URL could not be accessed: " . $reason; } - throw new TransportException($message, $httpStatus); + throw new TransportException($message, $statusCode); } // fail if we already have auth if ($this->io->hasAuthentication($origin)) { - throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $httpStatus); + throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); } $this->io->overwriteError(''); @@ -176,6 +189,12 @@ class AuthHelper return array('retry' => $retry, 'storeAuth' => $storeAuth); } + /** + * @param array $headers + * @param string $origin + * @param string $url + * @return array updated headers array + */ public function addAuthenticationHeader(array $headers, $origin, $url) { if ($this->io->hasAuthentication($origin)) { diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php index 2874524ed..c01677522 100644 --- a/src/Composer/Util/Url.php +++ b/src/Composer/Util/Url.php @@ -19,6 +19,12 @@ use Composer\Config; */ class Url { + /** + * @param Config $config + * @param string $url + * @param string $ref + * @return string the updated URL + */ public static function updateDistReference(Config $config, $url, $ref) { $host = parse_url($url, PHP_URL_HOST); @@ -53,6 +59,10 @@ class Url return $url; } + /** + * @param string $url + * @return string + */ public static function getOrigin(Config $config, $url) { if (0 === strpos($url, 'file://')) { @@ -89,5 +99,4 @@ class Url return $origin; } - } From ed65625126c42354f3b27a3ed922eb75de8f57d7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 28 Nov 2018 08:59:45 +0100 Subject: [PATCH 038/321] Handle custom http options cleaner in ComposerRepo --- src/Composer/Repository/ComposerRepository.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 8c92d3e4d..939fb84ba 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -102,11 +102,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); $this->versionParser = new VersionParser(); $this->loader = new ArrayLoader($this->versionParser); - if ($this->options) { - // TODO solve this somehow - should be sent at request time not on the instance - $httpDownloader = clone $httpDownloader; - $httpDownloader->setOptions($this->options); - } $this->httpDownloader = $httpDownloader; $this->eventDispatcher = $eventDispatcher; $this->repoConfig = $repoConfig; @@ -263,7 +258,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) { $url = str_replace(array('%query%', '%type%'), array($query, $type), $this->searchUrl); - $search = $this->httpDownloader->get($url)->decodeJson(); + $search = $this->httpDownloader->get($url, $this->options)->decodeJson(); if (empty($search['results'])) { return array(); @@ -826,7 +821,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); } - $response = $httpDownloader->get($filename); + $response = $httpDownloader->get($filename, $this->options); $json = $response->getBody(); if ($sha256 && $sha256 !== hash('sha256', $json)) { // undo downgrade before trying again if http seems to be hijacked or modifying content somehow @@ -917,7 +912,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); } - $options = array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))); + $options = $this->options; + if (isset($options['http']['header'])) { + $options['http']['header'] = (array) $options['http']['header']; + } + $options['http']['header'][] = array('If-Modified-Since: '.$lastModifiedTime); $response = $httpDownloader->get($filename, $options); $json = $response->getBody(); if ($json === '' && $response->getStatusCode() === 304) { From e67030076a135bb9e1c1f1201441128c581b307e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 28 Nov 2018 09:36:05 +0100 Subject: [PATCH 039/321] Fix show command --- src/Composer/Command/ShowCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 1dc5876a8..9604bdc71 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -553,7 +553,7 @@ EOT $matches[$index] = $package->getId(); } - $pool = $repositorySet->createPool(); + $pool = $repositorySet->createPoolForPackage($package->getName()); // select preferred package according to policy rules if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) { From 0961e16795dffb5a1ed10eac209e28e44dfcdb2e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 29 Nov 2018 19:31:41 +0100 Subject: [PATCH 040/321] Add support for new metadata-url repo attribute --- .../Repository/ComposerRepository.php | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 939fb84ba..101c23bcb 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -667,6 +667,17 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']); } + // metadata-url indiates V2 repo protocol so it takes over from all the V1 types + // V2 only has lazyProviders and no ability to process anything else, plus support for async loading + if (!empty($data['metadata-url'])) { + $this->lazyProvidersUrl = $this->canonicalizeUrl($data['metadata-url']); + $this->providersUrl = null; + $this->hasProviders = false; + $this->hasPartialPackages = false; + $this->allowSslDowngrade = false; + unset($data['providers-url'], $data['providers'], $data['providers-includes']); + } + if ($this->allowSslDowngrade) { $this->url = str_replace('https://', 'http://', $this->url); $this->baseUrl = str_replace('https://', 'http://', $this->baseUrl); @@ -681,22 +692,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasProviders = true; } - // TODO this is for testing only, remove once packagist reports v2 protocol support - if (preg_match('{^https?://repo\.packagist\.org/?$}i', $this->url)) { - $this->repoConfig['force-lazy-providers'] = true; - } - - // force values for packagist - if (preg_match('{^https?://repo\.packagist\.org/?$}i', $this->url) && !empty($this->repoConfig['force-lazy-providers'])) { - $this->url = 'https://repo.packagist.org'; - $this->baseUrl = 'https://repo.packagist.org'; - $this->lazyProvidersUrl = $this->canonicalizeUrl('https://repo.packagist.org/p/%package%.json'); - $this->providersUrl = null; - } elseif (!empty($this->repoConfig['force-lazy-providers'])) { - $this->lazyProvidersUrl = $this->canonicalizeUrl('/p/%package%.json'); - $this->providersUrl = null; - } - return $this->rootData = $data; } From e753bf08b1cd3c2438a7e9aca0b94f5957626704 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 09:34:32 +0100 Subject: [PATCH 041/321] Minor tweaks --- composer.lock | 2 +- src/Composer/DependencyResolver/Solver.php | 1 + src/Composer/Repository/ComposerRepository.php | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 63b8033b9..a22e11a6b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d356b92e869790db1e9d2c0f4b10935e", + "content-hash": "3243ce6f26231df34d1bceab1a148803", "packages": [ { "name": "composer/ca-bundle", diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index aa5432188..c3128c6c4 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -217,6 +217,7 @@ class Solver $this->setupInstalledMap(); + $this->io->writeError('Generating rules', true, IOInterface::DEBUG); $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs); $this->checkForRootRequireProblems($ignorePlatformReqs); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 101c23bcb..a491bec72 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -527,6 +527,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito // TODO what if not, then throw? if ($this->lazyProvidersUrl) { foreach ($packageNames as $name => $constraint) { + $name = strtolower($name); + // skip platform packages, root package and composer-plugin-api if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { continue; From e8c694877047f331e042c17a4ac3b63b1a25962d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 11:20:35 +0100 Subject: [PATCH 042/321] Deduplicate link instances between versions of a given package --- src/Composer/Package/Loader/ArrayLoader.php | 155 ++++++++++++---- .../Repository/ComposerRepository.php | 170 ++++++++++-------- .../Repository/ComposerRepositoryTest.php | 17 +- 3 files changed, 230 insertions(+), 112 deletions(-) diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 49ba45aa8..ac91b18fe 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -38,6 +38,70 @@ class ArrayLoader implements LoaderInterface } public function load(array $config, $class = 'Composer\Package\CompletePackage') + { + $package = $this->createObject($config, $class); + + foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { + if (isset($config[$type])) { + $method = 'set'.ucfirst($opts['method']); + $package->{$method}( + $this->parseLinks( + $package->getName(), + $package->getPrettyVersion(), + $opts['description'], + $config[$type] + ) + ); + } + } + + $package = $this->configureObject($package, $config); + + return $package; + } + + public function loadPackages(array $versions, $class) + { + static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); + + $packages = array(); + $linkCache = array(); + + foreach ($versions as $version) { + if (isset($version['versions'])) { + $baseVersion = $version; + foreach ($uniqKeys as $key) { + unset($baseVersion[$key.'s']); + } + + foreach ($version['versions'] as $index => $dummy) { + $unpackedVersion = $baseVersion; + foreach ($uniqKeys as $key) { + $unpackedVersion[$key] = $version[$key.'s'][$index]; + } + + $package = $this->createObject($unpackedVersion, $class); + + $this->configureCachedLinks($linkCache, $package, $unpackedVersion); + $package = $this->configureObject($package, $unpackedVersion); + + $packages[] = $package; + } + } else { + $package = $this->createObject($version, $class); + + $this->configureCachedLinks($linkCache, $package, $version); + $package = $this->configureObject($package, $version); + + $packages[] = $package; + } + + } + + return $packages; + } + + private function createObject(array $config, $class) { if (!isset($config['name'])) { throw new \UnexpectedValueException('Unknown package has no name defined ('.json_encode($config).').'); @@ -52,7 +116,12 @@ class ArrayLoader implements LoaderInterface } else { $version = $this->versionParser->normalize($config['version']); } - $package = new $class($config['name'], $version, $config['version']); + + return new $class($config['name'], $version, $config['version']); + } + + private function configureObject($package, array $config) + { $package->setType(isset($config['type']) ? strtolower($config['type']) : 'library'); if (isset($config['target-dir'])) { @@ -109,20 +178,6 @@ class ArrayLoader implements LoaderInterface } } - foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { - if (isset($config[$type])) { - $method = 'set'.ucfirst($opts['method']); - $package->{$method}( - $this->parseLinks( - $package->getName(), - $package->getPrettyVersion(), - $opts['description'], - $config[$type] - ) - ); - } - } - if (isset($config['suggest']) && is_array($config['suggest'])) { foreach ($config['suggest'] as $target => $reason) { if ('self.version' === trim($reason)) { @@ -202,21 +257,50 @@ class ArrayLoader implements LoaderInterface } } - if ($aliasNormalized = $this->getBranchAlias($config)) { - if ($package instanceof RootPackageInterface) { - $package = new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); - } else { - $package = new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); - } - } - if ($this->loadOptions && isset($config['transport-options'])) { $package->setTransportOptions($config['transport-options']); } + if ($aliasNormalized = $this->getBranchAlias($config)) { + if ($package instanceof RootPackageInterface) { + return new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + } + + return new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + } + return $package; } + private function configureCachedLinks(&$linkCache, $package, array $config) + { + $name = $package->getName(); + $prettyVersion = $package->getPrettyVersion(); + + foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { + if (isset($config[$type])) { + $method = 'set'.ucfirst($opts['method']); + + $links = array(); + foreach ($config[$type] as $prettyTarget => $constraint) { + $target = strtolower($prettyTarget); + if ($constraint === 'self.version') { + $links[$target] = $this->createLink($name, $prettyVersion, $opts['description'], $target, $constraint); + } else { + if (!isset($linkCache[$name][$type][$target][$constraint])) { + $linkCache[$name][$type][$target][$constraint] = array($target, $this->createLink($name, $prettyVersion, $opts['description'], $target, $constraint)); + } + + list($target, $link) = $linkCache[$name][$type][$target][$constraint]; + $links[$target] = $link; + } + } + + $package->{$method}($links); + } + } + } + /** * @param string $source source package name * @param string $sourceVersion source package version (pretty version ideally) @@ -228,21 +312,26 @@ class ArrayLoader implements LoaderInterface { $res = array(); foreach ($links as $target => $constraint) { - if (!is_string($constraint)) { - throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($constraint) . ' (' . var_export($constraint, true) . ')'); - } - if ('self.version' === $constraint) { - $parsedConstraint = $this->versionParser->parseConstraints($sourceVersion); - } else { - $parsedConstraint = $this->versionParser->parseConstraints($constraint); - } - - $res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint); + $res[strtolower($target)] = $this->createLink($source, $sourceVersion, $description, $target, $constraint); } return $res; } + private function createLink($source, $sourceVersion, $description, $target, $prettyConstraint) + { + if (!is_string($prettyConstraint)) { + throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($prettyConstraint) . ' (' . var_export($prettyConstraint, true) . ')'); + } + if ('self.version' === $prettyConstraint) { + $parsedConstraint = $this->versionParser->parseConstraints($sourceVersion); + } else { + $parsedConstraint = $this->versionParser->parseConstraints($prettyConstraint); + } + + return new Link($source, $target, $parsedConstraint, $description, $prettyConstraint); + } + /** * Retrieves a branch alias (dev-master => 1.0.x-dev for example) if it exists * diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index a491bec72..06c984bd5 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -61,7 +61,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $rootData; private $hasPartialPackages; private $partialPackagesByName; - private $versionParser; + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * @private + */ + public $versionParser; public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { @@ -414,6 +418,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->providers[$name] = array(); foreach ($packages['packages'] as $versions) { + $versionsToLoad = array(); foreach ($versions as $version) { if (!$loadingPartialPackage && $this->hasPartialPackages && isset($this->partialPackagesByName[$version['name']])) { continue; @@ -440,40 +445,44 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito continue; } - // load acceptable packages in the providers - $package = $this->createPackage($version, 'Composer\Package\CompletePackage'); - $package->setRepository($this); + $versionsToLoad[] = $version; + } + } - if ($package instanceof AliasPackage) { - $aliased = $package->getAliasOf(); - $aliased->setRepository($this); + // load acceptable packages in the providers + $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + foreach ($loadedPackages as $package) { + $package->setRepository($this); - $this->providers[$name][$version['uid']] = $aliased; - $this->providers[$name][$version['uid'].'-alias'] = $package; + if ($package instanceof AliasPackage) { + $aliased = $package->getAliasOf(); + $aliased->setRepository($this); - // override provider with its alias so it can be expanded in the if block above - $this->providersByUid[$version['uid']] = $package; - } else { - $this->providers[$name][$version['uid']] = $package; - $this->providersByUid[$version['uid']] = $package; - } + $this->providers[$name][$version['uid']] = $aliased; + $this->providers[$name][$version['uid'].'-alias'] = $package; - // handle root package aliases - unset($rootAliasData); + // override provider with its alias so it can be expanded in the if block above + $this->providersByUid[$version['uid']] = $package; + } else { + $this->providers[$name][$version['uid']] = $package; + $this->providersByUid[$version['uid']] = $package; + } - if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { - $rootAliasData = $this->rootAliases[$package->getName()][$package->getVersion()]; - } elseif ($package instanceof AliasPackage && isset($this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()])) { - $rootAliasData = $this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()]; - } + // handle root package aliases + unset($rootAliasData); - if (isset($rootAliasData)) { - $alias = $this->createAliasPackage($package, $rootAliasData['alias_normalized'], $rootAliasData['alias']); - $alias->setRepository($this); + if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { + $rootAliasData = $this->rootAliases[$package->getName()][$package->getVersion()]; + } elseif ($package instanceof AliasPackage && isset($this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()])) { + $rootAliasData = $this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()]; + } - $this->providers[$name][$version['uid'].'-root'] = $alias; - $this->providersByUid[$version['uid'].'-root'] = $alias; - } + if (isset($rootAliasData)) { + $alias = $this->createAliasPackage($package, $rootAliasData['alias_normalized'], $rootAliasData['alias']); + $alias->setRepository($this); + + $this->providers[$name][$version['uid'].'-root'] = $alias; + $this->providersByUid[$version['uid'].'-root'] = $alias; } } } @@ -501,8 +510,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $repoData = $this->loadDataFromServer(); - foreach ($repoData as $package) { - $this->addPackage($this->createPackage($package, 'Composer\Package\CompletePackage')); + foreach ($this->createPackages($repoData, 'Composer\Package\CompletePackage') as $package) { + $this->addPackage($package); } } @@ -545,6 +554,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->asyncFetchFile($url, $cacheKey, $lastModified) ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) { + static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); + if (true === $response) { $response = $contents; } @@ -553,24 +564,37 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return; } - $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); + $versionsToLoad = array(); foreach ($response['packages'][$name] as $version) { - if (isset($version['versions'])) { - $baseVersion = $version; - foreach ($uniqKeys as $key) { - unset($baseVersion[$key.'s']); - } - - foreach ($version['versions'] as $index => $dummy) { - $unpackedVersion = $baseVersion; - foreach ($uniqKeys as $key) { - $unpackedVersion[$key] = $version[$key.'s'][$index]; + if (isset($version['version_normalizeds'])) { + foreach ($version['version_normalizeds'] as $index => $normalizedVersion) { + if (!$repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $normalizedVersion)) { + foreach ($uniqKeys as $key) { + unset($version[$key.'s'][$index]); + } } - - $repo->createPackageIfAcceptable($packages, $isPackageAcceptableCallable, $unpackedVersion, $constraint); + } + if (count($version['version_normalizeds'])) { + $versionsToLoad[] = $version; } } else { - $repo->createPackageIfAcceptable($packages, $isPackageAcceptableCallable, $version, $constraint); + if (!isset($version['version_normalized'])) { + $version['version_normalized'] = $repo->versionParser->normalize($version['version']); + } + + if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $version['version_normalized'])) { + $versionsToLoad[] = $version; + } + } + } + + $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + foreach ($loadedPackages as $package) { + $package->setRepository($this); + + $packages[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { + $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } } }, function ($e) { @@ -592,27 +616,17 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito * * @private */ - public function createPackageIfAcceptable(&$packages, $isPackageAcceptableCallable, $version, $constraint) + public function isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $versionNormalized) { - if (!call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { - return; + if (!call_user_func($isPackageAcceptableCallable, strtolower($name), VersionParser::parseStability($versionNormalized))) { + return false; } - if (isset($version['version_normalized']) && $constraint && !$constraint->matches(new Constraint('==', $version['version_normalized']))) { - return; + if ($constraint && !$constraint->matches(new Constraint('==', $versionNormalized))) { + return false; } - // load acceptable packages in the providers - $package = $this->createPackage($version, 'Composer\Package\CompletePackage'); - $package->setRepository($this); - - // if there was no version_normalized, then we need to check now for the constraint - if (!$constraint || isset($version['version_normalized']) || $constraint->matches(new Constraint('==', $package->getVersion()))) { - $packages[spl_object_hash($package)] = $package; - if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { - $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); - } - } + return true; } protected function loadRootServerFile() @@ -775,23 +789,37 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $packages; } - protected function createPackage(array $data, $class = 'Composer\Package\CompletePackage') + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * + * @private + */ + public function createPackages(array $packages, $class = 'Composer\Package\CompletePackage') { + if (!$packages) { + return; + } + try { - if (!isset($data['notification-url'])) { - $data['notification-url'] = $this->notifyUrl; + foreach ($packages as &$data) { + if (!isset($data['notification-url'])) { + $data['notification-url'] = $this->notifyUrl; + } } - $package = $this->loader->load($data, $class); - if (isset($this->sourceMirrors[$package->getSourceType()])) { - $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); - } - $package->setDistMirrors($this->distMirrors); - $this->configurePackageTransportOptions($package); + $packages = $this->loader->loadPackages($packages, $class); - return $package; + foreach ($packages as $package) { + if (isset($this->sourceMirrors[$package->getSourceType()])) { + $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); + } + $package->setDistMirrors($this->distMirrors); + $this->configurePackageTransportOptions($package); + } + + return $packages; } catch (\Exception $e) { - throw new \RuntimeException('Could not load package '.(isset($data['name']) ? $data['name'] : json_encode($data)).' in '.$this->url.': ['.get_class($e).'] '.$e->getMessage(), 0, $e); + throw new \RuntimeException('Could not load packages '.(isset($packages[0]['name']) ? $packages[0]['name'] : json_encode($packages)).' in '.$this->url.': ['.get_class($e).'] '.$e->getMessage(), 0, $e); } } diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 0ffd70751..3594de101 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -32,7 +32,7 @@ class ComposerRepositoryTest extends TestCase ); $repository = $this->getMockBuilder('Composer\Repository\ComposerRepository') - ->setMethods(array('loadRootServerFile', 'createPackage')) + ->setMethods(array('loadRootServerFile', 'createPackages')) ->setConstructorArgs(array( $repoConfig, new NullIO, @@ -47,16 +47,17 @@ class ComposerRepositoryTest extends TestCase ->method('loadRootServerFile') ->will($this->returnValue($repoPackages)); + $stubs = array(); foreach ($expected as $at => $arg) { - $stubPackage = $this->getPackage('stub/stub', '1.0.0'); - - $repository - ->expects($this->at($at + 2)) - ->method('createPackage') - ->with($this->identicalTo($arg), $this->equalTo('Composer\Package\CompletePackage')) - ->will($this->returnValue($stubPackage)); + $stubs[] = $this->getPackage('stub/stub', '1.0.0'); } + $repository + ->expects($this->at(2)) + ->method('createPackages') + ->with($this->identicalTo($expected), $this->equalTo('Composer\Package\CompletePackage')) + ->will($this->returnValue($stubs)); + // Triggers initialization $packages = $repository->getPackages(); From fc03ab9bbae520a38bdc71f2d5620edd90967893 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 11:36:15 +0100 Subject: [PATCH 043/321] Add COMPOSER_DISABLE_NETWORK env var for debugging --- doc/03-cli.md | 5 +++++ src/Composer/Repository/ComposerRepository.php | 7 ++++++- src/Composer/Util/HttpDownloader.php | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index 74374ec6b..f0b3dca35 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -928,4 +928,9 @@ repository options. Defaults to `1`. If set to `0`, Composer will not create `.htaccess` files in the composer home, cache, and data directories. +### COMPOSER_DISABLE_NETWORK + +If set to `1`, disables network access (best effort). This can be used for debugging or +to run Composer on a plane or a starship with poor connectivity. + ← [Libraries](02-libraries.md) | [Schema](04-schema.md) → diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 06c984bd5..624621d79 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1044,7 +1044,12 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return false; } - if (--$retries) { + // special error code returned when network is being artificially disabled + if ($e instanceof TransportException && $e->getStatusCode() === 499) { + $retries = 0; + } + + if (--$retries > 0) { usleep(100000); return $httpDownloader->add($filename, $options)->then($accept, $reject); diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 0e882c4a3..f2308d75a 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -16,6 +16,7 @@ use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; +use Composer\Util\Http\Response; use Psr\Log\LoggerInterface; use React\Promise\Promise; @@ -40,6 +41,7 @@ class HttpDownloader private $curl; private $rfs; private $idGen = 0; + private $disabled; /** * @param IOInterface $io The IO instance @@ -51,6 +53,8 @@ class HttpDownloader { $this->io = $io; + $this->disabled = (bool) getenv('COMPOSER_DISABLE_NETWORK'); + // Setup TLS options // The cafile option can be set via config.json if ($disableTls === false) { @@ -216,6 +220,17 @@ class HttpDownloader $options = $job['request']['options']; $origin = $job['origin']; + if ($this->disabled) { + if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) { + $resolve(new Response(array('url' => $url), 304, array(), '')); + } else { + $e = new TransportException('Network disabled', 499); + $e->setStatusCode(499); + $reject($e); + } + return; + } + if ($job['request']['copyTo']) { $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); } else { From 00de0f58541b09d863e025d0b881256ef1d63ae8 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 11:38:26 +0100 Subject: [PATCH 044/321] Fix 5.3 support --- src/Composer/Repository/ComposerRepository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 624621d79..89b7c78f6 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -588,9 +588,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } - $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + $loadedPackages = $repo->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); foreach ($loadedPackages as $package) { - $package->setRepository($this); + $package->setRepository($repo); $packages[spl_object_hash($package)] = $package; if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { From 14d6bcedda172844cda2dcc2ccb15c86a653d61b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 15:17:52 +0100 Subject: [PATCH 045/321] Fix redirect handling and some output tweaks --- src/Composer/Util/Http/CurlDownloader.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 4015b0bac..2528b9e3f 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -108,7 +108,7 @@ class CurlDownloader { $attributes = array_merge(array( 'retryAuthFailure' => true, - 'redirects' => 1, + 'redirects' => 0, 'storeAuth' => false, ), $attributes); @@ -195,7 +195,9 @@ class CurlDownloader $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : ''; $ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : ''; - $this->io->writeError('Downloading ' . $url . $usingProxy . $ifModified, true, IOInterface::DEBUG); + if ($attributes['redirects'] === 0) { + $this->io->writeError('Downloading ' . $url . $usingProxy . $ifModified, true, IOInterface::DEBUG); + } $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); // TODO progress @@ -249,6 +251,7 @@ class CurlDownloader if ($job['filename']) { fclose($job['bodyHandle']); $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $job['filename'].'~'); + $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); } else { rewind($job['bodyHandle']); $contents = stream_get_contents($job['bodyHandle']); @@ -268,7 +271,7 @@ class CurlDownloader } // handle 3xx redirects, 304 Not Modified is excluded - if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['redirects'] < $this->maxRedirects) { + if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['attributes']['redirects'] < $this->maxRedirects) { $location = $this->handleRedirect($job, $response); if ($location) { $this->restartJob($job, $location, array('redirects' => $job['attributes']['redirects'] + 1)); @@ -360,8 +363,7 @@ class CurlDownloader } if (!empty($targetUrl)) { - $this->io->writeError('', true, IOInterface::DEBUG); - $this->io->writeError(sprintf('Following redirect (%u) %s', $job['redirects'] + 1, $targetUrl), true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, $targetUrl), true, IOInterface::DEBUG); return $targetUrl; } @@ -429,7 +431,7 @@ class CurlDownloader $attributes = array_merge($job['attributes'], $attributes); $origin = Url::getOrigin($this->config, $url); - $this->initDownload($job['resolve'], $job['reject'], $origin, $url, $job['originalOptions'], $job['filename'], $attributes); + $this->initDownload($job['resolve'], $job['reject'], $origin, $url, $job['options'], $job['filename'], $attributes); } private function failResponse(array $job, Response $response, $errorMessage) From b47330adf10738e2533b774e39f75fcec40365c2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 17:03:56 +0100 Subject: [PATCH 046/321] Refactor ComposerRepository to work with combined repos having lazy providers and partial packages --- src/Composer/Command/ShowCommand.php | 4 +- .../Repository/ComposerRepository.php | 338 ++++++++++-------- src/Composer/Util/Http/CurlDownloader.php | 217 ++++++----- .../Repository/ComposerRepositoryTest.php | 4 +- 4 files changed, 296 insertions(+), 267 deletions(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 9604bdc71..5cb3fa860 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -317,8 +317,8 @@ EOT } else { $type = 'available'; } - if ($repo instanceof ComposerRepository && $repo->hasProviders()) { - foreach ($repo->getProviderNames() as $name) { + if ($repo instanceof ComposerRepository) { + foreach ($repo->getPackageNames() as $name) { if (!$packageFilter || preg_match($packageFilter, $name)) { $packages[$type][$name] = $name; } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 89b7c78f6..ff2895564 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -35,13 +35,13 @@ use Composer\Semver\Constraint\EmptyConstraint; */ class ComposerRepository extends ArrayRepository implements ConfigurableRepositoryInterface { - protected $config; - protected $repoConfig; - protected $options; - protected $url; - protected $baseUrl; - protected $io; - protected $httpDownloader; + private $config; + private $repoConfig; + private $options; + private $url; + private $baseUrl; + private $io; + private $httpDownloader; protected $cache; protected $notifyUrl; protected $searchUrl; @@ -49,14 +49,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito protected $providersUrl; protected $lazyProvidersUrl; protected $providerListing; - protected $providers = array(); - protected $providersByUid = array(); + private $providers = array(); + private $providersByUid = array(); protected $loader; - protected $rootAliases; - protected $allowSslDowngrade = false; - protected $eventDispatcher; - protected $sourceMirrors; - protected $distMirrors; + private $rootAliases; + private $allowSslDowngrade = false; + private $eventDispatcher; + private $sourceMirrors; + private $distMirrors; private $degradedMode = false; private $rootData; private $hasPartialPackages; @@ -134,28 +134,23 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $constraint = $this->versionParser->parseConstraints($constraint); } - // TODO we need a new way for the repo to report this v2 protocol somehow if ($this->lazyProvidersUrl) { + if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) { + return $this->filterPackages($this->whatProvides($name), $name, $constraint, true); + } + return $this->loadAsyncPackages(array($name => $constraint), function ($name, $stability) { return true; }); } + if (!$hasProviders) { return parent::findPackage($name, $constraint); } foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - $packages = $this->whatProvides($providerName); - foreach ($packages as $package) { - if ($name === $package->getName()) { - $pkgConstraint = new Constraint('==', $package->getVersion()); - if ($constraint->matches($pkgConstraint)) { - return $package; - } - } - } - break; + return $this->filterPackages($this->whatProvides($providerName), $name, $constraint, true); } } } @@ -168,40 +163,58 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito // this call initializes loadRootServerFile which is needed for the rest below to work $hasProviders = $this->hasProviders(); - // TODO we need a new way for the repo to report this v2 protocol somehow - if ($this->lazyProvidersUrl) { - return $this->loadAsyncPackages(array($name => $constraint ?: new EmptyConstraint()), function ($name, $stability) { - return true; - }); - } - if (!$hasProviders) { - return parent::findPackages($name, $constraint); - } - - // normalize name $name = strtolower($name); - if (null !== $constraint && !$constraint instanceof ConstraintInterface) { $constraint = $this->versionParser->parseConstraints($constraint); } - $packages = array(); + if ($this->lazyProvidersUrl) { + if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) { + return $this->filterPackages($this->whatProvides($name), $name, $constraint); + } + + return $this->loadAsyncPackages(array($name => $constraint ?: new EmptyConstraint()), function ($name, $stability) { + return true; + }); + } + + if (!$hasProviders) { + return parent::findPackages($name, $constraint); + } foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - $candidates = $this->whatProvides($providerName); - foreach ($candidates as $package) { - if ($name === $package->getName()) { - $pkgConstraint = new Constraint('==', $package->getVersion()); - if (null === $constraint || $constraint->matches($pkgConstraint)) { - $packages[] = $package; - } - } - } - break; + return $this->filterPackages($this->whatProvides($providerName), $name, $constraint); } } + return array(); + } + + private function filterPackages(array $packages, $name, $constraint = null, $returnFirstMatch = false) + { + $packages = array(); + + foreach ($packages as $package) { + // TODO this check can be removed once providers are only what really has that name anyway + if ($name !== $package->getName()) { + continue; + } + + $pkgConstraint = new Constraint('==', $package->getVersion()); + if (null === $constraint || $constraint->matches($pkgConstraint)) { + if ($returnFirstMatch) { + return $package; + } + + $packages[] = $package; + } + } + + if ($returnFirstMatch) { + return null; + } + return $packages; } @@ -214,39 +227,63 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return parent::getPackages(); } + public function getPackageNames() + { + if ($this->hasProviders()) { + return $this->getProviderNames(); + } + + // TODO implement new list API endpoint for repos somehow? + // TODO add getPackageNames to the RepositoryInterface perhaps? With filtering capability embedded? + + return $this->getPackages(); + } + public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable) { // this call initializes loadRootServerFile which is needed for the rest below to work $hasProviders = $this->hasProviders(); - // TODO we need a new way for the repo to report this v2 protocol somehow - if ($this->lazyProvidersUrl) { - return $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable); - } - if (!$hasProviders) { + if (!$hasProviders && !$this->hasPartialPackages() && !$this->lazyProvidersUrl) { return parent::loadPackages($packageNameMap, $isPackageAcceptableCallable); } $packages = array(); - foreach ($packageNameMap as $name => $constraint) { - $matches = array(); - $candidates = $this->whatProvides($name, false, $isPackageAcceptableCallable); - foreach ($candidates as $candidate) { - if ($candidate->getName() === $name && (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion())))) { - $matches[spl_object_hash($candidate)] = $candidate; - if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) { - $matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf(); - } + + if ($hasProviders || $this->hasPartialPackages()) { + foreach ($packageNameMap as $name => $constraint) { + $matches = array(); + + if (!$hasProviders && !isset($this->partialPackagesByName[$name])) { + continue; } - } - foreach ($candidates as $candidate) { - if ($candidate instanceof AliasPackage) { - if (isset($result[spl_object_hash($candidate->getAliasOf())])) { + + $candidates = $this->whatProvides($name, $isPackageAcceptableCallable); + foreach ($candidates as $candidate) { + if ($candidate->getName() === $name && (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion())))) { $matches[spl_object_hash($candidate)] = $candidate; + if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) { + $matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf(); + } } } + foreach ($candidates as $candidate) { + if ($candidate instanceof AliasPackage) { + if (isset($result[spl_object_hash($candidate->getAliasOf())])) { + $matches[spl_object_hash($candidate)] = $candidate; + } + } + } + $packages = array_merge($packages, $matches); + + unset($packageNameMap[$name]); } - $packages = array_merge($packages, $matches); + + return $packages; + } + + if ($this->lazyProvidersUrl) { + $packages = array_merge($packages, $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable)); } return $packages; @@ -295,7 +332,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return parent::search($query, $mode); } - public function getProviderNames() + private function getProviderNames() { $this->loadRootServerFile(); @@ -315,7 +352,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return array(); } - protected function configurePackageTransportOptions(PackageInterface $package) + private function configurePackageTransportOptions(PackageInterface $package) { foreach ($package->getDistUrls() as $url) { if (strpos($url, $this->baseUrl) === 0) { @@ -326,7 +363,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } - public function hasProviders() + private function hasProviders() { $this->loadRootServerFile(); @@ -335,21 +372,16 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito /** * @param string $name package name - * @param bool $bypassFilters If set to true, this bypasses the stability filtering, and forces a recompute without cache * @param callable $isPackageAcceptableCallable * @return array|mixed */ - public function whatProvides($name, $bypassFilters = false, $isPackageAcceptableCallable = null) + private function whatProvides($name, $isPackageAcceptableCallable = null) { - if (isset($this->providers[$name]) && !$bypassFilters) { + if (isset($this->providers[$name])) { return $this->providers[$name]; } - if ($this->hasPartialPackages && null === $this->partialPackagesByName) { - $this->initializePartialPackages(); - } - - if (!$this->hasPartialPackages || !isset($this->partialPackagesByName[$name])) { + if (!$this->hasPartialPackages() || !isset($this->partialPackagesByName[$name])) { // skip platform packages, root package and composer-plugin-api if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { return array(); @@ -420,7 +452,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($packages['packages'] as $versions) { $versionsToLoad = array(); foreach ($versions as $version) { - if (!$loadingPartialPackage && $this->hasPartialPackages && isset($this->partialPackagesByName[$version['name']])) { + if (!$loadingPartialPackage && $this->hasPartialPackages() && isset($this->partialPackagesByName[$version['name']])) { continue; } @@ -441,7 +473,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } } else { - if (!$bypassFilters && $isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { + if ($isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { continue; } @@ -489,15 +521,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $result = $this->providers[$name]; - // clean up the cache because otherwise using this puts the repo in an inconsistent state with a polluted unfiltered cache - // which is likely not an issue but might cause hard to track behaviors depending on how the repo is used - if ($bypassFilters) { - foreach ($this->providers[$name] as $uid => $provider) { - unset($this->providersByUid[$uid]); - } - unset($this->providers[$name]); - } - return $result; } @@ -533,76 +556,76 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $packages = array(); $repo = $this; - // TODO what if not, then throw? - if ($this->lazyProvidersUrl) { - foreach ($packageNames as $name => $constraint) { - $name = strtolower($name); + if (!$this->lazyProvidersUrl) { + throw new \LogicException('loadAsyncPackages only supports v2 protocol composer repos with a metadata-url'); + } - // skip platform packages, root package and composer-plugin-api - if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { - continue; - } + foreach ($packageNames as $name => $constraint) { + $name = strtolower($name); - $url = str_replace('%package%', $name, $this->lazyProvidersUrl); - $cacheKey = 'provider-'.strtr($name, '/', '$').'.json'; + // skip platform packages, root package and composer-plugin-api + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { + continue; + } - $lastModified = null; - if ($contents = $this->cache->read($cacheKey)) { - $contents = json_decode($contents, true); - $lastModified = isset($contents['last-modified']) ? $contents['last-modified'] : null; - } + $url = str_replace('%package%', $name, $this->lazyProvidersUrl); + $cacheKey = 'provider-'.strtr($name, '/', '$').'.json'; - $this->asyncFetchFile($url, $cacheKey, $lastModified) - ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) { - static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); + $lastModified = null; + if ($contents = $this->cache->read($cacheKey)) { + $contents = json_decode($contents, true); + $lastModified = isset($contents['last-modified']) ? $contents['last-modified'] : null; + } - if (true === $response) { - $response = $contents; - } + $this->asyncFetchFile($url, $cacheKey, $lastModified) + ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) { + static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); - if (!isset($response['packages'][$name])) { - return; - } + if (true === $response) { + $response = $contents; + } - $versionsToLoad = array(); - foreach ($response['packages'][$name] as $version) { - if (isset($version['version_normalizeds'])) { - foreach ($version['version_normalizeds'] as $index => $normalizedVersion) { - if (!$repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $normalizedVersion)) { - foreach ($uniqKeys as $key) { - unset($version[$key.'s'][$index]); - } + if (!isset($response['packages'][$name])) { + return; + } + + $versionsToLoad = array(); + foreach ($response['packages'][$name] as $version) { + if (isset($version['version_normalizeds'])) { + foreach ($version['version_normalizeds'] as $index => $normalizedVersion) { + if (!$repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $normalizedVersion)) { + foreach ($uniqKeys as $key) { + unset($version[$key.'s'][$index]); } } - if (count($version['version_normalizeds'])) { - $versionsToLoad[] = $version; - } - } else { - if (!isset($version['version_normalized'])) { - $version['version_normalized'] = $repo->versionParser->normalize($version['version']); - } + } + if (count($version['version_normalizeds'])) { + $versionsToLoad[] = $version; + } + } else { + if (!isset($version['version_normalized'])) { + $version['version_normalized'] = $repo->versionParser->normalize($version['version']); + } - if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $version['version_normalized'])) { - $versionsToLoad[] = $version; - } + if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $version['version_normalized'])) { + $versionsToLoad[] = $version; } } + } - $loadedPackages = $repo->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); - foreach ($loadedPackages as $package) { - $package->setRepository($repo); + $loadedPackages = $repo->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + foreach ($loadedPackages as $package) { + $package->setRepository($repo); - $packages[spl_object_hash($package)] = $package; - if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { - $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); - } + $packages[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { + $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } - }, function ($e) { - // TODO use ->done() above instead with react/promise 2.0 - var_dump('Uncaught Ex', $e->getMessage()); - throw $e; - }); - } + } + }, function ($e) { + // TODO use ->done() above instead with react/promise 2.0 + throw $e; + }); } $this->httpDownloader->wait(); @@ -684,12 +707,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } // metadata-url indiates V2 repo protocol so it takes over from all the V1 types - // V2 only has lazyProviders and no ability to process anything else, plus support for async loading + // V2 only has lazyProviders and possibly partial packages, but no ability to process anything else, + // V2 also supports async loading if (!empty($data['metadata-url'])) { $this->lazyProvidersUrl = $this->canonicalizeUrl($data['metadata-url']); $this->providersUrl = null; $this->hasProviders = false; - $this->hasPartialPackages = false; + $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']); $this->allowSslDowngrade = false; unset($data['providers-url'], $data['providers'], $data['providers-includes']); } @@ -711,7 +735,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $this->rootData = $data; } - protected function canonicalizeUrl($url) + private function canonicalizeUrl($url) { if ('/' === $url[0]) { return preg_replace('{(https?://[^/]+).*}i', '$1' . $url, $this->url); @@ -720,14 +744,23 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $url; } - protected function loadDataFromServer() + private function loadDataFromServer() { $data = $this->loadRootServerFile(); return $this->loadIncludes($data); } - protected function loadProviderListings($data) + private function hasPartialPackages() + { + if ($this->hasPartialPackages && null === $this->partialPackagesByName) { + $this->initializePartialPackages(); + } + + return $this->hasPartialPackages; + } + + private function loadProviderListings($data) { if (isset($data['providers'])) { if (!is_array($this->providerListing)) { @@ -752,7 +785,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } - protected function loadIncludes($data) + private function loadIncludes($data) { $packages = array(); @@ -924,7 +957,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $data; } - protected function fetchFileIfLastModified($filename, $cacheKey, $lastModifiedTime) + private function fetchFileIfLastModified($filename, $cacheKey, $lastModifiedTime) { $retries = 3; while ($retries--) { @@ -990,7 +1023,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } - protected function asyncFetchFile($filename, $cacheKey, $lastModifiedTime = null) + private function asyncFetchFile($filename, $cacheKey, $lastModifiedTime = null) { $retries = 3; $httpDownloader = $this->httpDownloader; @@ -1039,7 +1072,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito }; $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, $cache, &$degradedMode) { - var_dump('Caught8', $e->getMessage()); if ($e instanceof TransportException && $e->getStatusCode() === 404) { return false; } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 2528b9e3f..1ff8393c9 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -211,132 +211,127 @@ class CurlDownloader } $active = true; - try { - $this->checkCurlResult(curl_multi_exec($this->multiHandle, $active)); - if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) { - // sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select - usleep(150); + $this->checkCurlResult(curl_multi_exec($this->multiHandle, $active)); + if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) { + // sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select + usleep(150); + } + + while ($progress = curl_multi_info_read($this->multiHandle)) { + $curlHandle = $progress['handle']; + $i = (int) $curlHandle; + if (!isset($this->jobs[$i])) { + continue; } + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); + $job = $this->jobs[$i]; + unset($this->jobs[$i]); + curl_multi_remove_handle($this->multiHandle, $curlHandle); + $error = curl_error($curlHandle); + $errno = curl_errno($curlHandle); + curl_close($curlHandle); - while ($progress = curl_multi_info_read($this->multiHandle)) { - $curlHandle = $progress['handle']; - $i = (int) $curlHandle; - if (!isset($this->jobs[$i])) { - continue; + $headers = null; + $statusCode = null; + $response = null; + try { +// TODO progress + //$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']); + if (CURLE_OK !== $errno) { + throw new TransportException($error); } - $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); - $job = $this->jobs[$i]; - unset($this->jobs[$i]); - curl_multi_remove_handle($this->multiHandle, $curlHandle); - $error = curl_error($curlHandle); - $errno = curl_errno($curlHandle); - curl_close($curlHandle); - $headers = null; - $statusCode = null; - $response = null; - try { -// TODO progress - //$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']); - if (CURLE_OK !== $errno) { - throw new TransportException($error); - } + $statusCode = $progress['http_code']; + rewind($job['headerHandle']); + $headers = explode("\r\n", rtrim(stream_get_contents($job['headerHandle']))); + fclose($job['headerHandle']); - $statusCode = $progress['http_code']; - rewind($job['headerHandle']); - $headers = explode("\r\n", rtrim(stream_get_contents($job['headerHandle']))); - fclose($job['headerHandle']); + // prepare response object + if ($job['filename']) { + fclose($job['bodyHandle']); + $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $job['filename'].'~'); + $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); + } else { + rewind($job['bodyHandle']); + $contents = stream_get_contents($job['bodyHandle']); + fclose($job['bodyHandle']); + $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); + $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); + } - // prepare response object - if ($job['filename']) { - fclose($job['bodyHandle']); - $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $job['filename'].'~'); - $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); - } else { - rewind($job['bodyHandle']); - $contents = stream_get_contents($job['bodyHandle']); - fclose($job['bodyHandle']); - $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); - $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); - } - - $result = $this->isAuthenticatedRetryNeeded($job, $response); - if ($result['retry']) { - if ($job['filename']) { - @unlink($job['filename'].'~'); - } - - $this->restartJob($job, $job['url'], array('storeAuth' => $result['storeAuth'])); - continue; - } - - // handle 3xx redirects, 304 Not Modified is excluded - if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['attributes']['redirects'] < $this->maxRedirects) { - $location = $this->handleRedirect($job, $response); - if ($location) { - $this->restartJob($job, $location, array('redirects' => $job['attributes']['redirects'] + 1)); - continue; - } - } - - // fail 4xx and 5xx responses and capture the response - if ($statusCode >= 400 && $statusCode <= 599) { - throw $this->failResponse($job, $response, $response->getStatusMessage()); -// TODO progress -// $this->io->overwriteError("Downloading (failed)", false); - } - - if ($job['attributes']['storeAuth']) { - $this->authHelper->storeAuth($job['origin'], $job['attributes']['storeAuth']); - } - - // resolve promise - if ($job['filename']) { - rename($job['filename'].'~', $job['filename']); - call_user_func($job['resolve'], true); - } else { - call_user_func($job['resolve'], $response); - } - } catch (\Exception $e) { - if ($e instanceof TransportException && $headers) { - $e->setHeaders($headers); - $e->setStatusCode($statusCode); - } - if ($e instanceof TransportException && $response) { - $e->setResponse($response->getBody()); - } - - if (is_resource($job['headerHandle'])) { - fclose($job['headerHandle']); - } - if (is_resource($job['bodyHandle'])) { - fclose($job['bodyHandle']); - } + $result = $this->isAuthenticatedRetryNeeded($job, $response); + if ($result['retry']) { if ($job['filename']) { @unlink($job['filename'].'~'); } - call_user_func($job['reject'], $e); - } - } - foreach ($this->jobs as $i => $curlHandle) { - if (!isset($this->jobs[$i])) { + $this->restartJob($job, $job['url'], array('storeAuth' => $result['storeAuth'])); continue; } - $curlHandle = $this->jobs[$i]['curlHandle']; - $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); - if ($this->jobs[$i]['progress'] !== $progress) { - $previousProgress = $this->jobs[$i]['progress']; - $this->jobs[$i]['progress'] = $progress; - - // TODO - //$this->onProgress($curlHandle, $this->jobs[$i]['callback'], $progress, $previousProgress); + // handle 3xx redirects, 304 Not Modified is excluded + if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['attributes']['redirects'] < $this->maxRedirects) { + $location = $this->handleRedirect($job, $response); + if ($location) { + $this->restartJob($job, $location, array('redirects' => $job['attributes']['redirects'] + 1)); + continue; + } } + + // fail 4xx and 5xx responses and capture the response + if ($statusCode >= 400 && $statusCode <= 599) { + throw $this->failResponse($job, $response, $response->getStatusMessage()); +// TODO progress +// $this->io->overwriteError("Downloading (failed)", false); + } + + if ($job['attributes']['storeAuth']) { + $this->authHelper->storeAuth($job['origin'], $job['attributes']['storeAuth']); + } + + // resolve promise + if ($job['filename']) { + rename($job['filename'].'~', $job['filename']); + call_user_func($job['resolve'], true); + } else { + call_user_func($job['resolve'], $response); + } + } catch (\Exception $e) { + if ($e instanceof TransportException && $headers) { + $e->setHeaders($headers); + $e->setStatusCode($statusCode); + } + if ($e instanceof TransportException && $response) { + $e->setResponse($response->getBody()); + } + + if (is_resource($job['headerHandle'])) { + fclose($job['headerHandle']); + } + if (is_resource($job['bodyHandle'])) { + fclose($job['bodyHandle']); + } + if ($job['filename']) { + @unlink($job['filename'].'~'); + } + call_user_func($job['reject'], $e); + } + } + + foreach ($this->jobs as $i => $curlHandle) { + if (!isset($this->jobs[$i])) { + continue; + } + $curlHandle = $this->jobs[$i]['curlHandle']; + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); + + if ($this->jobs[$i]['progress'] !== $progress) { + $previousProgress = $this->jobs[$i]['progress']; + $this->jobs[$i]['progress'] = $progress; + + // TODO + //$this->onProgress($curlHandle, $this->jobs[$i]['callback'], $progress, $previousProgress); } - } catch (\Exception $e) { - // TODO - var_dump('Caught2', get_class($e), $e->getMessage(), $e);die; } } diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 3594de101..552c767a5 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -153,7 +153,9 @@ class ComposerRepositoryTest extends TestCase ), )); - $packages = $repo->whatProvides('a', false, array($this, 'isPackageAcceptableReturnTrue')); + $reflMethod = new \ReflectionMethod($repo, 'whatProvides'); + $reflMethod->setAccessible(true); + $packages = $reflMethod->invoke($repo, 'a', array($this, 'isPackageAcceptableReturnTrue')); $this->assertCount(7, $packages); $this->assertEquals(array('1', '1-alias', '2', '2-alias', '2-root', '3', '3-root'), array_keys($packages)); From 137c32e72e45ec2c9cbdfb3dc7226897fe1ea92f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 17:27:23 +0100 Subject: [PATCH 047/321] Do not prohibit http for old provider URLs on .org in case they are used --- src/Composer/Util/Http/CurlDownloader.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 1ff8393c9..45b1d21de 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -114,8 +114,10 @@ class CurlDownloader $originalOptions = $options; - // check URL can be accessed (i.e. is not insecure) - $this->config->prohibitUrlByConfig($url, $this->io); + // check URL can be accessed (i.e. is not insecure), but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 + if (!preg_match('{^http://(repo\.)?packagist\.org/p/}', $url) || (false === strpos($url, '$') && false === strpos($url, '%24'))) { + $this->config->prohibitUrlByConfig($url, $this->io); + } $curlHandle = curl_init(); $headerHandle = fopen('php://temp/maxmemory:32768', 'w+b'); From a5d5270a7e8db39baf141bccdfc4c4875ed0f1db Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Dec 2018 17:37:39 +0100 Subject: [PATCH 048/321] Make sure other providers of a name are never loaded --- .../Repository/ComposerRepository.php | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index ff2895564..420bde535 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -136,7 +136,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->lazyProvidersUrl) { if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) { - return $this->filterPackages($this->whatProvides($name), $name, $constraint, true); + return $this->filterPackages($this->whatProvides($name), $constraint, true); } return $this->loadAsyncPackages(array($name => $constraint), function ($name, $stability) { @@ -150,7 +150,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - return $this->filterPackages($this->whatProvides($providerName), $name, $constraint, true); + return $this->filterPackages($this->whatProvides($providerName), $constraint, true); } } } @@ -170,7 +170,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->lazyProvidersUrl) { if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) { - return $this->filterPackages($this->whatProvides($name), $name, $constraint); + return $this->filterPackages($this->whatProvides($name), $constraint); } return $this->loadAsyncPackages(array($name => $constraint ?: new EmptyConstraint()), function ($name, $stability) { @@ -184,22 +184,18 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { - return $this->filterPackages($this->whatProvides($providerName), $name, $constraint); + return $this->filterPackages($this->whatProvides($providerName), $constraint); } } return array(); } - private function filterPackages(array $packages, $name, $constraint = null, $returnFirstMatch = false) + private function filterPackages(array $packages, $constraint = null, $returnFirstMatch = false) { $packages = array(); foreach ($packages as $package) { - // TODO this check can be removed once providers are only what really has that name anyway - if ($name !== $package->getName()) { - continue; - } $pkgConstraint = new Constraint('==', $package->getVersion()); if (null === $constraint || $constraint->matches($pkgConstraint)) { @@ -452,7 +448,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($packages['packages'] as $versions) { $versionsToLoad = array(); foreach ($versions as $version) { - if (!$loadingPartialPackage && $this->hasPartialPackages() && isset($this->partialPackagesByName[$version['name']])) { + $normalizedName = strtolower($version['name']); + + // only load the actual named package, not other packages that might find themselves in the same file + if ($normalizedName !== $name) { + continue; + } + + if (!$loadingPartialPackage && $this->hasPartialPackages() && isset($this->partialPackagesByName[$normalizedName])) { continue; } @@ -473,7 +476,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } } else { - if ($isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, strtolower($version['name']), VersionParser::parseStability($version['version']))) { + if ($isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, $normalizedName, VersionParser::parseStability($version['version']))) { continue; } @@ -830,7 +833,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito public function createPackages(array $packages, $class = 'Composer\Package\CompletePackage') { if (!$packages) { - return; + return array(); } try { @@ -1110,19 +1113,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->partialPackagesByName = array(); foreach ($rootData['packages'] as $package => $versions) { - $package = strtolower($package); foreach ($versions as $version) { - $this->partialPackagesByName[$package][] = $version; - if (!empty($version['provide']) && is_array($version['provide'])) { - foreach ($version['provide'] as $provided => $providedVersion) { - $this->partialPackagesByName[strtolower($provided)][] = $version; - } - } - if (!empty($version['replace']) && is_array($version['replace'])) { - foreach ($version['replace'] as $provided => $providedVersion) { - $this->partialPackagesByName[strtolower($provided)][] = $version; - } - } + $this->partialPackagesByName[strtolower($version['name'])][] = $version; } } From 4b7658a2a86ee9f834dd022de1e25487e4a140c4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 5 Dec 2018 13:28:37 +0100 Subject: [PATCH 049/321] Small tweaks and make sure composer fails properly in a plane --- src/Composer/Repository/ComposerRepository.php | 9 +++++++-- src/Composer/Util/Http/CurlDownloader.php | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 420bde535..43087ee7f 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -250,13 +250,18 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($packageNameMap as $name => $constraint) { $matches = array(); + // if a repo has no providers but only partial packages and the partial packages are missing + // then we don't want to call whatProvides as it would try to load from the providers and fail if (!$hasProviders && !isset($this->partialPackagesByName[$name])) { continue; } $candidates = $this->whatProvides($name, $isPackageAcceptableCallable); foreach ($candidates as $candidate) { - if ($candidate->getName() === $name && (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion())))) { + if ($candidate->getName() !== $name) { + throw new \LogicException('whatProvides should never return a package with a different name than the requested one'); + } + if (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { $matches[spl_object_hash($candidate)] = $candidate; if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) { $matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf(); @@ -278,7 +283,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $packages; } - if ($this->lazyProvidersUrl) { + if ($this->lazyProvidersUrl && count($packageNameMap)) { $packages = array_merge($packages, $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable)); } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 45b1d21de..3dc6710b6 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -225,6 +225,7 @@ class CurlDownloader if (!isset($this->jobs[$i])) { continue; } + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); $job = $this->jobs[$i]; unset($this->jobs[$i]); @@ -239,7 +240,7 @@ class CurlDownloader try { // TODO progress //$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']); - if (CURLE_OK !== $errno) { + if (CURLE_OK !== $errno || $error) { throw new TransportException($error); } From fd5c5ff6bc4b7662d342efebace527295f5f4160 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 14 Jan 2019 15:30:55 +0100 Subject: [PATCH 050/321] Fix implementation of whatProvides for older provider-only repos --- src/Composer/Package/Loader/ArrayLoader.php | 1 - .../Repository/ComposerRepository.php | 92 +++++-------------- .../Repository/ComposerRepositoryTest.php | 13 +-- 3 files changed, 24 insertions(+), 82 deletions(-) diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index ac91b18fe..9fed111e0 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -95,7 +95,6 @@ class ArrayLoader implements LoaderInterface $packages[] = $package; } - } return $packages; diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 43087ee7f..829883b58 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -49,10 +49,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito protected $providersUrl; protected $lazyProvidersUrl; protected $providerListing; - private $providers = array(); - private $providersByUid = array(); protected $loader; - private $rootAliases; private $allowSslDowngrade = false; private $eventDispatcher; private $sourceMirrors; @@ -116,11 +113,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $this->repoConfig; } - public function setRootAliases(array $rootAliases) - { - $this->rootAliases = $rootAliases; - } - /** * {@inheritDoc} */ @@ -378,10 +370,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito */ private function whatProvides($name, $isPackageAcceptableCallable = null) { - if (isset($this->providers[$name])) { - return $this->providers[$name]; - } - if (!$this->hasPartialPackages() || !isset($this->partialPackagesByName[$name])) { // skip platform packages, root package and composer-plugin-api if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { @@ -449,9 +437,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $loadingPartialPackage = true; } - $this->providers[$name] = array(); + $result = array(); + $versionsToLoad = array(); foreach ($packages['packages'] as $versions) { - $versionsToLoad = array(); foreach ($versions as $version) { $normalizedName = strtolower($version['name']); @@ -464,70 +452,34 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito continue; } - // avoid loading the same objects twice - if (isset($this->providersByUid[$version['uid']])) { - // skip if already assigned - if (!isset($this->providers[$name][$version['uid']])) { - // expand alias in two packages - if ($this->providersByUid[$version['uid']] instanceof AliasPackage) { - $this->providers[$name][$version['uid']] = $this->providersByUid[$version['uid']]->getAliasOf(); - $this->providers[$name][$version['uid'].'-alias'] = $this->providersByUid[$version['uid']]; - } else { - $this->providers[$name][$version['uid']] = $this->providersByUid[$version['uid']]; - } - // check for root aliases - if (isset($this->providersByUid[$version['uid'].'-root'])) { - $this->providers[$name][$version['uid'].'-root'] = $this->providersByUid[$version['uid'].'-root']; - } - } - } else { + if (!isset($versionsToLoad[$version['uid']])) { if ($isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, $normalizedName, VersionParser::parseStability($version['version']))) { continue; } - $versionsToLoad[] = $version; - } - } - - // load acceptable packages in the providers - $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); - foreach ($loadedPackages as $package) { - $package->setRepository($this); - - if ($package instanceof AliasPackage) { - $aliased = $package->getAliasOf(); - $aliased->setRepository($this); - - $this->providers[$name][$version['uid']] = $aliased; - $this->providers[$name][$version['uid'].'-alias'] = $package; - - // override provider with its alias so it can be expanded in the if block above - $this->providersByUid[$version['uid']] = $package; - } else { - $this->providers[$name][$version['uid']] = $package; - $this->providersByUid[$version['uid']] = $package; - } - - // handle root package aliases - unset($rootAliasData); - - if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { - $rootAliasData = $this->rootAliases[$package->getName()][$package->getVersion()]; - } elseif ($package instanceof AliasPackage && isset($this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()])) { - $rootAliasData = $this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()]; - } - - if (isset($rootAliasData)) { - $alias = $this->createAliasPackage($package, $rootAliasData['alias_normalized'], $rootAliasData['alias']); - $alias->setRepository($this); - - $this->providers[$name][$version['uid'].'-root'] = $alias; - $this->providersByUid[$version['uid'].'-root'] = $alias; + $versionsToLoad[$version['uid']] = $version; } } } - $result = $this->providers[$name]; + // load acceptable packages in the providers + $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); + $uids = array_keys($versionsToLoad); + + foreach ($loadedPackages as $index => $package) { + $package->setRepository($this); + $uid = $uids[$index]; + + if ($package instanceof AliasPackage) { + $aliased = $package->getAliasOf(); + $aliased->setRepository($this); + + $result[$uid] = $aliased; + $result[$uid.'-alias'] = $package; + } else { + $result[$uid] = $package; + } + } return $result; } diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 552c767a5..55ca6bf09 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -146,21 +146,12 @@ class ComposerRepositoryTest extends TestCase ))); $versionParser = new VersionParser(); - $repo->setRootAliases(array( - 'a' => array( - $versionParser->normalize('0.6') => array('alias' => 'dev-feature', 'alias_normalized' => $versionParser->normalize('dev-feature')), - $versionParser->normalize('1.1.x-dev') => array('alias' => '1.0', 'alias_normalized' => $versionParser->normalize('1.0')), - ), - )); - $reflMethod = new \ReflectionMethod($repo, 'whatProvides'); $reflMethod->setAccessible(true); $packages = $reflMethod->invoke($repo, 'a', array($this, 'isPackageAcceptableReturnTrue')); - $this->assertCount(7, $packages); - $this->assertEquals(array('1', '1-alias', '2', '2-alias', '2-root', '3', '3-root'), array_keys($packages)); - $this->assertInstanceOf('Composer\Package\AliasPackage', $packages['2-root']); - $this->assertSame($packages['2'], $packages['2-root']->getAliasOf()); + $this->assertCount(5, $packages); + $this->assertEquals(array('1', '1-alias', '2', '2-alias', '3'), array_keys($packages)); $this->assertSame($packages['2'], $packages['2-alias']->getAliasOf()); } From c97b7a9be5701ea606ef3eecb01016903f9d25ac Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 14 Jan 2019 16:37:33 +0100 Subject: [PATCH 051/321] Fix implementation of filterPackages --- src/Composer/Repository/ComposerRepository.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 829883b58..6e3994380 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -185,17 +185,25 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private function filterPackages(array $packages, $constraint = null, $returnFirstMatch = false) { - $packages = array(); + if (null === $constraint) { + if ($returnFirstMatch) { + return reset($packages); + } + + return $packages; + } + + $filteredPackages = array(); foreach ($packages as $package) { - $pkgConstraint = new Constraint('==', $package->getVersion()); - if (null === $constraint || $constraint->matches($pkgConstraint)) { + + if ($constraint->matches($pkgConstraint)) { if ($returnFirstMatch) { return $package; } - $packages[] = $package; + $filteredPackages[] = $package; } } @@ -203,7 +211,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return null; } - return $packages; + return $filteredPackages; } public function getPackages() From 0f2f950cb6aa583b60c7b63845734dce185874a7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 15 Jan 2019 11:40:49 +0100 Subject: [PATCH 052/321] Add available-packages key for new repo format, and many consistency tweaks/fixes across various repo formats --- .../Repository/ComposerRepository.php | 108 ++++++++++++++---- 1 file changed, 83 insertions(+), 25 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 6e3994380..8fe6a04ed 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -47,6 +47,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito protected $searchUrl; protected $hasProviders = false; protected $providersUrl; + protected $availablePackages; protected $lazyProvidersUrl; protected $providerListing; protected $loader; @@ -131,20 +132,28 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $this->filterPackages($this->whatProvides($name), $constraint, true); } - return $this->loadAsyncPackages(array($name => $constraint), function ($name, $stability) { + if (is_array($this->availablePackages) && !isset($this->availablePackages[$name])) { + return; + } + + $packages = $this->loadAsyncPackages(array($name => $constraint), function ($name, $stability) { return true; }); + + return reset($packages); } - if (!$hasProviders) { - return parent::findPackage($name, $constraint); - } - - foreach ($this->getProviderNames() as $providerName) { - if ($name === $providerName) { - return $this->filterPackages($this->whatProvides($providerName), $constraint, true); + if ($hasProviders) { + foreach ($this->getProviderNames() as $providerName) { + if ($name === $providerName) { + return $this->filterPackages($this->whatProvides($providerName), $constraint, true); + } } + + return; } + + return parent::findPackage($name, $constraint); } /** @@ -165,22 +174,26 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $this->filterPackages($this->whatProvides($name), $constraint); } + if (is_array($this->availablePackages) && !isset($this->availablePackages[$name])) { + return array(); + } + return $this->loadAsyncPackages(array($name => $constraint ?: new EmptyConstraint()), function ($name, $stability) { return true; }); } - if (!$hasProviders) { - return parent::findPackages($name, $constraint); - } - - foreach ($this->getProviderNames() as $providerName) { - if ($name === $providerName) { - return $this->filterPackages($this->whatProvides($providerName), $constraint); + if ($hasProviders) { + foreach ($this->getProviderNames() as $providerName) { + if ($name === $providerName) { + return $this->filterPackages($this->whatProvides($providerName), $constraint); + } } + + return array(); } - return array(); + return parent::findPackages($name, $constraint); } private function filterPackages(array $packages, $constraint = null, $returnFirstMatch = false) @@ -216,7 +229,22 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito public function getPackages() { - if ($this->hasProviders()) { + $hasProviders = $this->hasProviders(); + + if ($this->lazyProvidersUrl) { + if (is_array($this->availablePackages)) { + $packageMap = array(); + foreach ($this->availablePackages as $name) { + $packageMap[$name] = new EmptyConstraint(); + } + + return array_values($this->loadAsyncPackages($packageMap, function ($name, $stability) { return true; })); + } + + throw new \LogicException('Composer repositories that have lazy providers and no available-packages list can not load the complete list of packages, use getProviderNames instead.'); + } + + if ($hasProviders) { throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getProviderNames instead.'); } @@ -225,14 +253,28 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito public function getPackageNames() { - if ($this->hasProviders()) { + // TODO add getPackageNames to the RepositoryInterface perhaps? With filtering capability embedded? + $hasProviders = $this->hasProviders(); + + if ($this->lazyProvidersUrl) { + if (is_array($this->availablePackages)) { + return array_keys($this->availablePackages); + } + + // TODO implement new list API endpoint for those repos somehow? + return array(); + } + + if ($hasProviders) { return $this->getProviderNames(); } - // TODO implement new list API endpoint for repos somehow? - // TODO add getPackageNames to the RepositoryInterface perhaps? With filtering capability embedded? + $names = array(); + foreach ($this->getPackages() as $package) { + $names[] = $package->getPrettyName(); + } - return $this->getPackages(); + return $names; } public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable) @@ -279,11 +321,16 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito unset($packageNameMap[$name]); } - - return $packages; } if ($this->lazyProvidersUrl && count($packageNameMap)) { + if (is_array($this->availablePackages)) { + $availPackages = $this->availablePackages; + $packageNameMap = array_filter($packageNameMap, function ($name) use ($availPackages) { + return isset($availPackages[strtolower($name)]); + }, ARRAY_FILTER_USE_KEY); + } + $packages = array_merge($packages, $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable)); } @@ -317,11 +364,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $results; } - if ($this->hasProviders()) { + if ($this->hasProviders() || $this->lazyProvidersUrl) { $results = array(); $regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i'; - foreach ($this->getProviderNames() as $name) { + foreach ($this->getPackageNames() as $name) { if (preg_match($regex, $name)) { $results[] = array('name' => $name); } @@ -683,6 +730,17 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasProviders = false; $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']); $this->allowSslDowngrade = false; + + // provides a list of package names that are available in this repo + // this disables lazy-provider behavior in the sense that if a list is available we assume it is finite and won't search for other packages in that repo + // while if no list is there lazyProvidersUrl is used when looking for any package name to see if the repo knows it + if (!empty($data['available-packages'])) { + $availPackages = array_map('strtolower', $data['available-packages']); + $this->availablePackages = array_combine($availPackages, $availPackages); + } + + // Remove legacy keys as most repos need to be compatible with Composer v1 + // as well but we are not interested in the old format anymore at this point unset($data['providers-url'], $data['providers'], $data['providers-includes']); } From 3dfcae99a901c8743272e93a0698b4838439041c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 17 Jan 2019 17:12:33 +0100 Subject: [PATCH 053/321] Add parallel download capability to FileDownloader and derivatives --- src/Composer/Command/ArchiveCommand.php | 6 +- src/Composer/Command/CreateProjectCommand.php | 22 +- src/Composer/Command/StatusCommand.php | 2 +- src/Composer/Downloader/ArchiveDownloader.php | 106 +++---- src/Composer/Downloader/DownloadManager.php | 224 ++++++++++----- .../Downloader/DownloaderInterface.php | 10 +- src/Composer/Downloader/FileDownloader.php | 221 ++++++++------ src/Composer/Downloader/FossilDownloader.php | 2 +- src/Composer/Downloader/GitDownloader.php | 2 +- src/Composer/Downloader/GzipDownloader.php | 13 +- src/Composer/Downloader/HgDownloader.php | 2 +- src/Composer/Downloader/PathDownloader.php | 9 + .../Downloader/PerforceDownloader.php | 2 +- src/Composer/Downloader/PharDownloader.php | 4 +- src/Composer/Downloader/RarDownloader.php | 3 +- src/Composer/Downloader/SvnDownloader.php | 2 +- src/Composer/Downloader/TarDownloader.php | 4 +- src/Composer/Downloader/VcsDownloader.php | 12 +- src/Composer/Downloader/XzDownloader.php | 10 +- src/Composer/Downloader/ZipDownloader.php | 2 +- src/Composer/Factory.php | 14 +- .../Installer/InstallationManager.php | 27 +- src/Composer/Installer/InstallerInterface.php | 10 + src/Composer/Installer/LibraryInstaller.php | 10 +- .../Installer/MetapackageInstaller.php | 8 + src/Composer/Installer/NoopInstaller.php | 7 + src/Composer/Installer/PluginInstaller.php | 10 +- src/Composer/Installer/ProjectInstaller.php | 13 +- .../Package/Archiver/ArchiveManager.php | 9 +- .../Repository/ComposerRepository.php | 13 +- src/Composer/Util/Http/CurlDownloader.php | 2 +- src/Composer/Util/HttpDownloader.php | 6 +- src/Composer/Util/Loop.php | 47 +++ tests/Composer/Test/ComposerTest.php | 2 +- .../Test/Downloader/ArchiveDownloaderTest.php | 2 +- .../Test/Downloader/DownloadManagerTest.php | 271 ++++++++---------- .../Test/Downloader/FileDownloaderTest.php | 39 ++- .../Test/Downloader/FossilDownloaderTest.php | 4 +- .../Test/Downloader/GitDownloaderTest.php | 12 +- .../Test/Downloader/HgDownloaderTest.php | 4 +- .../Downloader/PerforceDownloaderTest.php | 4 +- .../Test/Downloader/XzDownloaderTest.php | 9 +- .../Test/Downloader/ZipDownloaderTest.php | 43 +-- .../EventDispatcher/EventDispatcherTest.php | 2 +- .../Installer/InstallationManagerTest.php | 41 ++- .../Test/Installer/LibraryInstallerTest.php | 2 +- tests/Composer/Test/Mock/FactoryMock.php | 5 +- .../Test/Mock/InstallationManagerMock.php | 13 + .../Package/Archiver/ArchiveManagerTest.php | 6 +- .../Test/Plugin/PluginInstallerTest.php | 2 +- 50 files changed, 803 insertions(+), 492 deletions(-) create mode 100644 src/Composer/Util/Loop.php diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index f893ed679..e26aa59a1 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -22,6 +22,7 @@ use Composer\Script\ScriptEvents; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\Filesystem; +use Composer\Util\Loop; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -104,8 +105,9 @@ EOT $archiveManager = $composer->getArchiveManager(); } else { $factory = new Factory; - $downloadManager = $factory->createDownloadManager($io, $config, $factory->createHttpDownloader($io, $config)); - $archiveManager = $factory->createArchiveManager($config, $downloadManager); + $httpDownloader = $factory->createHttpDownloader($io, $config); + $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader); + $archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader)); } if ($packageName) { diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index a1c364539..e165649a6 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder; use Composer\Json\JsonFile; use Composer\Config\JsonConfigSource; use Composer\Util\Filesystem; +use Composer\Util\Loop; use Composer\Package\Version\VersionParser; /** @@ -345,15 +346,18 @@ EOT $package = $package->getAliasOf(); } - $dm = $this->createDownloadManager($io, $config); + $factory = new Factory(); + + $httpDownloader = $factory->createHttpDownloader($io, $config); + $dm = $factory->createDownloadManager($io, $config, $httpDownloader); $dm->setPreferSource($preferSource) ->setPreferDist($preferDist) ->setOutputProgress(!$noProgress); $projectInstaller = new ProjectInstaller($directory, $dm); - $im = $this->createInstallationManager(); + $im = $factory->createInstallationManager(new Loop($httpDownloader)); $im->addInstaller($projectInstaller); - $im->install(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package)); + $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package)); $im->notifyInstalls($io); // collect suggestions @@ -369,16 +373,4 @@ EOT return $installedFromVcs; } - - protected function createDownloadManager(IOInterface $io, Config $config) - { - $factory = new Factory(); - - return $factory->createDownloadManager($io, $config, $factory->createHttpDownloader($io, $config)); - } - - protected function createInstallationManager() - { - return new InstallationManager(); - } } diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index 3e46b7fa0..06fc7638b 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -89,7 +89,7 @@ EOT // list packages foreach ($installedRepo->getCanonicalPackages() as $package) { - $downloader = $dm->getDownloaderForInstalledPackage($package); + $downloader = $dm->getDownloaderForPackage($package); $targetDir = $im->getInstallPath($package); if ($downloader instanceof ChangeReportInterface) { diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index d041a7f88..3c53a086e 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -30,33 +30,50 @@ abstract class ArchiveDownloader extends FileDownloader * @throws \RuntimeException * @throws \UnexpectedValueException */ - public function download(PackageInterface $package, $path, $output = true) + public function install(PackageInterface $package, $path, $output = true) { - $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); - $retries = 3; - while ($retries--) { - $fileName = parent::download($package, $path, $output); + if ($output) { + $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + } - if ($output) { - $this->io->writeError(' Extracting archive', false, IOInterface::VERBOSE); + $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); + $fileName = $this->getFileName($package, $path); + + if ($output) { + $this->io->writeError(' Extracting archive', true, IOInterface::VERBOSE); + } + + try { + $this->filesystem->ensureDirectoryExists($temporaryDir); + try { + $this->extract($package, $fileName, $temporaryDir); + } catch (\Exception $e) { + // remove cache if the file was corrupted + parent::clearLastCacheWrite($package); + throw $e; } - try { - $this->filesystem->ensureDirectoryExists($temporaryDir); - try { - $this->extract($fileName, $temporaryDir); - } catch (\Exception $e) { - // remove cache if the file was corrupted - parent::clearLastCacheWrite($package); - throw $e; + $this->filesystem->unlink($fileName); + + $renameAsOne = false; + if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) { + $renameAsOne = true; + } + + $contentDir = $this->getFolderContent($temporaryDir); + $singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir)); + + if ($renameAsOne) { + // if the target $path is clear, we can rename the whole package in one go instead of looping over the contents + if ($singleDirAtTopLevel) { + $extractedDir = (string) reset($contentDir); + } else { + $extractedDir = $temporaryDir; } - - $this->filesystem->unlink($fileName); - - $contentDir = $this->getFolderContent($temporaryDir); - + $this->filesystem->rename($extractedDir, $path); + } else { // only one dir in the archive, extract its contents out of it - if (1 === count($contentDir) && is_dir(reset($contentDir))) { + if ($singleDirAtTopLevel) { $contentDir = $this->getFolderContent((string) reset($contentDir)); } @@ -65,35 +82,24 @@ abstract class ArchiveDownloader extends FileDownloader $file = (string) $file; $this->filesystem->rename($file, $path . '/' . basename($file)); } - - $this->filesystem->removeDirectory($temporaryDir); - if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { - $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/'); - } - if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { - $this->filesystem->removeDirectory($this->config->get('vendor-dir')); - } - } catch (\Exception $e) { - // clean up - $this->filesystem->removeDirectory($path); - $this->filesystem->removeDirectory($temporaryDir); - - // retry downloading if we have an invalid zip file - if ($retries && $e instanceof \UnexpectedValueException && class_exists('ZipArchive') && $e->getCode() === \ZipArchive::ER_NOZIP) { - $this->io->writeError(''); - if ($this->io->isDebug()) { - $this->io->writeError(' Invalid zip file ('.$e->getMessage().'), retrying...'); - } else { - $this->io->writeError(' Invalid zip file, retrying...'); - } - usleep(500000); - continue; - } - - throw $e; } - break; + $this->filesystem->removeDirectory($temporaryDir); + if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { + $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/'); + } + if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { + $this->filesystem->removeDirectory($this->config->get('vendor-dir')); + } + } catch (\Exception $e) { + // clean up + $this->filesystem->removeDirectory($path); + $this->filesystem->removeDirectory($temporaryDir); + if (file_exists($fileName)) { + $this->filesystem->unlink($fileName); + } + + throw $e; } } @@ -102,7 +108,7 @@ abstract class ArchiveDownloader extends FileDownloader */ protected function getFileName(PackageInterface $package, $path) { - return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); + return rtrim($path.'_'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); } /** @@ -113,7 +119,7 @@ abstract class ArchiveDownloader extends FileDownloader * * @throws \UnexpectedValueException If can not extract downloaded file to path */ - abstract protected function extract($file, $path); + abstract protected function extract(PackageInterface $package, $file, $path); /** * Returns the folder content, excluding dotfiles diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index 15c00a6e6..4bc865827 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -15,6 +15,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\IO\IOInterface; use Composer\Util\Filesystem; +use React\Promise\PromiseInterface; /** * Downloaders manager. @@ -24,6 +25,7 @@ use Composer\Util\Filesystem; class DownloadManager { private $io; + private $httpDownloader; private $preferDist = false; private $preferSource = false; private $packagePreferences = array(); @@ -33,9 +35,9 @@ class DownloadManager /** * Initializes download manager. * - * @param IOInterface $io The Input Output Interface - * @param bool $preferSource prefer downloading from source - * @param Filesystem|null $filesystem custom Filesystem object + * @param IOInterface $io The Input Output Interface + * @param bool $preferSource prefer downloading from source + * @param Filesystem|null $filesystem custom Filesystem object */ public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null) { @@ -140,7 +142,7 @@ class DownloadManager * wrong type * @return DownloaderInterface|null */ - public function getDownloaderForInstalledPackage(PackageInterface $package) + public function getDownloaderForPackage(PackageInterface $package) { $installationSource = $package->getInstallationSource(); @@ -154,7 +156,7 @@ class DownloadManager $downloader = $this->getDownloader($package->getSourceType()); } else { throw new \InvalidArgumentException( - 'Package '.$package.' seems not been installed properly' + 'Package '.$package.' does not have an installation source set' ); } @@ -171,63 +173,95 @@ class DownloadManager return $downloader; } + public function getDownloaderType(DownloaderInterface $downloader) + { + return array_search($downloader, $this->downloaders); + } + /** * Downloads package into target dir. * * @param PackageInterface $package package instance * @param string $targetDir target dir - * @param bool $preferSource prefer installation from source + * @param PackageInterface $prevPackage previous package instance in case of updates + * + * @return PromiseInterface + * @throws \InvalidArgumentException if package have no urls to download from + * @throws \RuntimeException + */ + public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) + { + $this->filesystem->ensureDirectoryExists(dirname($targetDir)); + + $sources = $this->getAvailableSources($package, $prevPackage); + + $io = $this->io; + $self = $this; + + $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download) { + $source = array_shift($sources); + if ($retry) { + $io->writeError(' Now trying to download from ' . $source . ''); + } + $package->setInstallationSource($source); + + $downloader = $self->getDownloaderForPackage($package); + if (!$downloader) { + return \React\Promise\resolve(); + } + + $handleError = function ($e) use ($sources, $source, $package, $io, $download) { + if ($e instanceof \RuntimeException) { + if (!$sources) { + throw $e; + } + + $io->writeError( + ' Failed to download '. + $package->getPrettyName(). + ' from ' . $source . ': '. + $e->getMessage().'' + ); + + return $download(true); + } + + throw $e; + }; + + try { + $result = $downloader->download($package, $targetDir); + } catch (\Exception $e) { + return $handleError($e); + } + if (!$result instanceof PromiseInterface) { + return \React\Promise\resolve($result); + } + + $res = $result->then(function ($res) { + return $res; + }, $handleError); + + return $res; + }; + + return $download(); + } + + /** + * Installs package into target dir. + * + * @param PackageInterface $package package instance + * @param string $targetDir target dir * * @throws \InvalidArgumentException if package have no urls to download from * @throws \RuntimeException */ - public function download(PackageInterface $package, $targetDir, $preferSource = null) + public function install(PackageInterface $package, $targetDir) { - $preferSource = null !== $preferSource ? $preferSource : $this->preferSource; - $sourceType = $package->getSourceType(); - $distType = $package->getDistType(); - - $sources = array(); - if ($sourceType) { - $sources[] = 'source'; - } - if ($distType) { - $sources[] = 'dist'; - } - - if (empty($sources)) { - throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified'); - } - - if (!$preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) { - $sources = array_reverse($sources); - } - - $this->filesystem->ensureDirectoryExists($targetDir); - - foreach ($sources as $i => $source) { - if (isset($e)) { - $this->io->writeError(' Now trying to download from ' . $source . ''); - } - $package->setInstallationSource($source); - try { - $downloader = $this->getDownloaderForInstalledPackage($package); - if ($downloader) { - $downloader->download($package, $targetDir); - } - break; - } catch (\RuntimeException $e) { - if ($i === count($sources) - 1) { - throw $e; - } - - $this->io->writeError( - ' Failed to download '. - $package->getPrettyName(). - ' from ' . $source . ': '. - $e->getMessage().'' - ); - } + $downloader = $this->getDownloaderForPackage($package); + if ($downloader) { + $downloader->install($package, $targetDir); } } @@ -242,31 +276,23 @@ class DownloadManager */ public function update(PackageInterface $initial, PackageInterface $target, $targetDir) { - $downloader = $this->getDownloaderForInstalledPackage($initial); + $downloader = $this->getDownloaderForPackage($target); + $initialDownloader = $this->getDownloaderForPackage($initial); + + // no downloaders present means update from metapackage to metapackage, nothing to do + if (!$initialDownloader && !$downloader) { + return; + } + + // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed if (!$downloader) { + $initialDownloader->remove($initial, $targetDir); return; } - $installationSource = $initial->getInstallationSource(); - - if ('dist' === $installationSource) { - $initialType = $initial->getDistType(); - $targetType = $target->getDistType(); - } else { - $initialType = $initial->getSourceType(); - $targetType = $target->getSourceType(); - } - - // upgrading from a dist stable package to a dev package, force source reinstall - if ($target->isDev() && 'dist' === $installationSource) { - $downloader->remove($initial, $targetDir); - $this->download($target, $targetDir); - - return; - } - + $initialType = $this->getDownloaderType($initialDownloader); + $targetType = $this->getDownloaderType($downloader); if ($initialType === $targetType) { - $target->setInstallationSource($installationSource); try { $downloader->update($initial, $target, $targetDir); @@ -282,8 +308,12 @@ class DownloadManager } } - $downloader->remove($initial, $targetDir); - $this->download($target, $targetDir, 'source' === $installationSource); + // if downloader type changed, or update failed and user asks for reinstall, + // we wipe the dir and do a new install instead of updating it + if ($initialDownloader) { + $initialDownloader->remove($initial, $targetDir); + } + $this->install($target, $targetDir); } /** @@ -294,7 +324,7 @@ class DownloadManager */ public function remove(PackageInterface $package, $targetDir) { - $downloader = $this->getDownloaderForInstalledPackage($package); + $downloader = $this->getDownloaderForPackage($package); if ($downloader) { $downloader->remove($package, $targetDir); } @@ -322,4 +352,48 @@ class DownloadManager return $package->isDev() ? 'source' : 'dist'; } + + /** + * @return string[] + */ + private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null) + { + $sourceType = $package->getSourceType(); + $distType = $package->getDistType(); + + // add source before dist by default + $sources = array(); + if ($sourceType) { + $sources[] = 'source'; + } + if ($distType) { + $sources[] = 'dist'; + } + + if (empty($sources)) { + throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified'); + } + + if ( + $prevPackage + // if we are updating, we want to keep the same source as the previously installed package (if available in the new one) + && in_array($prevPackage->getInstallationSource(), $sources, true) + // unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over + && !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev()) + ) { + $prevSource = $prevPackage->getInstallationSource(); + usort($sources, function ($a, $b) use ($prevSource) { + return $a === $prevSource ? -1 : 1; + }); + + return $sources; + } + + // reverse sources in case dist is the preferred source for this package + if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) { + $sources = array_reverse($sources); + } + + return $sources; + } } diff --git a/src/Composer/Downloader/DownloaderInterface.php b/src/Composer/Downloader/DownloaderInterface.php index 713bf36dc..ac56583c4 100644 --- a/src/Composer/Downloader/DownloaderInterface.php +++ b/src/Composer/Downloader/DownloaderInterface.php @@ -13,6 +13,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; +use React\Promise\PromiseInterface; /** * Downloader interface. @@ -29,13 +30,20 @@ interface DownloaderInterface */ public function getInstallationSource(); + /** + * This should do any network-related tasks to prepare for install/update + * + * @return PromiseInterface|null + */ + public function download(PackageInterface $package, $path); + /** * Downloads specific package into specific folder. * * @param PackageInterface $package package instance * @param string $path download path */ - public function download(PackageInterface $package, $path); + public function install(PackageInterface $package, $path); /** * Updates specific package in specific folder from initial to target version. diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 8b196e60c..3418eef84 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -26,6 +26,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; use Composer\Util\HttpDownloader; use Composer\Util\Url as UrlUtil; +use Composer\Downloader\TransportException; /** * Base downloader for files @@ -43,7 +44,10 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface protected $filesystem; protected $cache; protected $outputProgress = true; - private $lastCacheWrites = array(); + /** + * @private this is only public for php 5.3 support in closures + */ + public $lastCacheWrites = array(); private $eventDispatcher; /** @@ -87,108 +91,149 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface throw new \InvalidArgumentException('The given package is missing url information'); } - if ($output) { - $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): ", false); - } - + $retries = 3; $urls = $package->getDistUrls(); - while ($url = array_shift($urls)) { - try { - $fileName = $this->doDownload($package, $path, $url); - break; - } catch (\Exception $e) { - if ($this->io->isDebug()) { - $this->io->writeError(''); - $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); - } elseif (count($urls)) { - $this->io->writeError(''); - $this->io->writeError(' Failed, trying the next URL ('.$e->getCode().': '.$e->getMessage().')', false); - } - - if (!count($urls)) { - throw $e; - } - } + foreach ($urls as $index => $url) { + $processedUrl = $this->processUrl($package, $url); + $urls[$index] = array( + 'base' => $url, + 'processed' => $processedUrl, + 'cacheKey' => $this->getCacheKey($package, $processedUrl) + ); } - if ($output) { - $this->io->writeError(''); - } - - return $fileName; - } - - protected function doDownload(PackageInterface $package, $path, $url) - { $this->filesystem->emptyDirectory($path); - $fileName = $this->getFileName($package, $path); - $processedUrl = $this->processUrl($package, $url); + $io = $this->io; + $cache = $this->cache; + $originalHttpDownloader = $this->httpDownloader; + $eventDispatcher = $this->eventDispatcher; + $filesystem = $this->filesystem; + $self = $this; - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $processedUrl); - if ($this->eventDispatcher) { - $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - } - $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); + $accept = null; + $reject = null; + $download = function () use ($io, $output, $originalHttpDownloader, $cache, $eventDispatcher, $package, $fileName, $path, &$urls, &$accept, &$reject) { + $url = reset($urls); + + $httpDownloader = $originalHttpDownloader; + if ($eventDispatcher) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']); + $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); + } - try { $checksum = $package->getDistSha1Checksum(); - $cacheKey = $this->getCacheKey($package, $processedUrl); + $cacheKey = $url['cacheKey']; // use from cache if it is present and has a valid checksum or we have no checksum to check against - if ($this->cache && (!$checksum || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) { - $this->io->writeError('Loading from cache', false); + if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) { + if ($output) { + $io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache"); + } + $result = \React\Promise\resolve($fileName); } else { - // download if cache restore failed - if (!$this->outputProgress) { - $this->io->writeError('Downloading', false); + if ($output) { + $io->writeError(" - Downloading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); } - // try to download 3 times then fail hard - $retries = 3; - while ($retries--) { - try { - // TODO handle this->outputProgress - $httpDownloader->copy($processedUrl, $fileName, $package->getTransportOptions()); - break; - } catch (TransportException $e) { - // if we got an http response with a proper code, then requesting again will probably not help, abort - if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) { - throw $e; - } - $this->io->writeError(''); - $this->io->writeError(' Download failed, retrying...', true, IOInterface::VERBOSE); - usleep(500000); - } - } - - if (!$this->outputProgress) { - $this->io->writeError(' (100%)', false); - } - - if ($this->cache) { - $this->lastCacheWrites[$package->getName()] = $cacheKey; - $this->cache->copyFrom($cacheKey, $fileName); - } + $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions()) + ->then($accept, $reject); } - if (!file_exists($fileName)) { - throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the' - .' directory is writable and you have internet connectivity'); + return $result->then(function ($result) use ($fileName, $checksum, $url) { + // in case of retry, the first call's Promise chain finally calls this twice at the end, + // once with $result being the returned $fileName from $accept, and then once for every + // failed request with a null result, which can be skipped. + if (null === $result) { + return $fileName; + } + + if (!file_exists($fileName)) { + throw new \UnexpectedValueException($url['base'].' could not be saved to '.$fileName.', make sure the' + .' directory is writable and you have internet connectivity'); + } + + if ($checksum && hash_file('sha1', $fileName) !== $checksum) { + throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')'); + } + + return $fileName; + }); + }; + + $accept = function ($response) use ($io, $cache, $package, $fileName, $path, $self, &$urls) { + $url = reset($urls); + $cacheKey = $url['cacheKey']; + + if ($cache) { + $self->lastCacheWrites[$package->getName()] = $cacheKey; + $cache->copyFrom($cacheKey, $fileName); } - if ($checksum && hash_file('sha1', $fileName) !== $checksum) { - throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')'); - } - } catch (\Exception $e) { + $response->collect(); + + return $fileName; + }; + + $reject = function ($e) use ($io, &$urls, $download, $fileName, $path, $package, &$retries, $filesystem, $self) { // clean up - $this->filesystem->removeDirectory($path); - $this->clearLastCacheWrite($package); + $filesystem->removeDirectory($path); + $self->clearLastCacheWrite($package); + + if ($e instanceof TransportException) { + // if we got an http response with a proper code, then requesting again will probably not help, abort + if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) { + $retries = 0; + } + } + + // special error code returned when network is being artificially disabled + if ($e instanceof TransportException && $e->getStatusCode() === 499) { + $retries = 0; + $urls = array(); + } + + if ($retries) { + usleep(500000); + $retries--; + + return $download(); + } + + array_shift($urls); + if ($urls) { + if ($io->isDebug()) { + $io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); + $io->writeError(' Trying the next URL for '.$package->getName()); + } elseif (count($urls)) { + $io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')'); + } + + $retries = 3; + usleep(100000); + + return $download(); + } + throw $e; + }; + + return $download(); + } + + /** + * {@inheritDoc} + */ + public function install(PackageInterface $package, $path, $output = true) + { + if ($output) { + $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); } - return $fileName; + $this->filesystem->ensureDirectoryExists($path); + $this->filesystem->rename($this->getFileName($package, $path), $path . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME)); } /** @@ -201,7 +246,11 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface return $this; } - protected function clearLastCacheWrite(PackageInterface $package) + /** + * TODO mark private in v3 + * @protected This is public due to PHP 5.3 + */ + public function clearLastCacheWrite(PackageInterface $package) { if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) { $this->cache->remove($this->lastCacheWrites[$package->getName()]); @@ -222,7 +271,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); $this->remove($initial, $path, false); - $this->download($target, $path, false); + $this->install($target, $path, false); $this->io->writeError(''); } @@ -249,7 +298,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface */ protected function getFileName(PackageInterface $package, $path) { - return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); + return $path.'_'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); } /** @@ -299,7 +348,9 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $e = null; try { - $this->download($package, $targetDir.'_compare', false); + $res = $this->download($package, $targetDir.'_compare', false); + $this->httpDownloader->wait(); + $res = $this->install($package, $targetDir.'_compare', false); $comparer = new Comparer(); $comparer->setSource($targetDir.'_compare'); diff --git a/src/Composer/Downloader/FossilDownloader.php b/src/Composer/Downloader/FossilDownloader.php index 135e973e0..a814f89b7 100644 --- a/src/Composer/Downloader/FossilDownloader.php +++ b/src/Composer/Downloader/FossilDownloader.php @@ -23,7 +23,7 @@ class FossilDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + public function doInstall(PackageInterface $package, $path, $url) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 869d5330b..ff398f300 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -38,7 +38,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + public function doInstall(PackageInterface $package, $path, $url) { GitUtil::cleanEnv(); $path = $this->normalizePath($path); diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index f65fcf27d..9748b91ac 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -36,9 +36,10 @@ class GzipDownloader extends ArchiveDownloader parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { - $targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3)); + $filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME); + $targetFilepath = $path . DIRECTORY_SEPARATOR . $filename; // Try to use gunzip on *nix if (!Platform::isWindows()) { @@ -63,14 +64,6 @@ class GzipDownloader extends ArchiveDownloader $this->extractUsingExt($file, $targetFilepath); } - /** - * {@inheritdoc} - */ - protected function getFileName(PackageInterface $package, $path) - { - return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); - } - private function extractUsingExt($file, $targetFilepath) { $archiveFile = gzopen($file, 'rb'); diff --git a/src/Composer/Downloader/HgDownloader.php b/src/Composer/Downloader/HgDownloader.php index 2921cc4b7..add381a75 100644 --- a/src/Composer/Downloader/HgDownloader.php +++ b/src/Composer/Downloader/HgDownloader.php @@ -24,7 +24,7 @@ class HgDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + public function doInstall(PackageInterface $package, $path, $url) { $hgUtils = new HgUtils($this->io, $this->config, $this->process); diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index e7084bd97..ea583cfc0 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -61,6 +61,15 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $realUrl )); } + } + + /** + * {@inheritdoc} + */ + public function install(PackageInterface $package, $path, $output = true) + { + $url = $package->getDistUrl(); + $realUrl = realpath($url); // Get the transport options with default values $transportOptions = $package->getTransportOptions() + array('symlink' => null); diff --git a/src/Composer/Downloader/PerforceDownloader.php b/src/Composer/Downloader/PerforceDownloader.php index a472b84c6..df270417f 100644 --- a/src/Composer/Downloader/PerforceDownloader.php +++ b/src/Composer/Downloader/PerforceDownloader.php @@ -27,7 +27,7 @@ class PerforceDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + public function doInstall(PackageInterface $package, $path, $url) { $ref = $package->getSourceReference(); $label = $this->getLabelFromSourceReference($ref); diff --git a/src/Composer/Downloader/PharDownloader.php b/src/Composer/Downloader/PharDownloader.php index 13fec244b..62741ee0e 100644 --- a/src/Composer/Downloader/PharDownloader.php +++ b/src/Composer/Downloader/PharDownloader.php @@ -12,6 +12,8 @@ namespace Composer\Downloader; +use Composer\Package\PackageInterface; + /** * Downloader for phar files * @@ -22,7 +24,7 @@ class PharDownloader extends ArchiveDownloader /** * {@inheritDoc} */ - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { // Can throw an UnexpectedValueException $archive = new \Phar($file); diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index 6fe4cf27c..2ebc3bf18 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -20,6 +20,7 @@ use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; +use Composer\Package\PackageInterface; use RarArchive; /** @@ -39,7 +40,7 @@ class RarDownloader extends ArchiveDownloader parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { $processError = null; diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index e23958164..0aae163c6 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -28,7 +28,7 @@ class SvnDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path, $url) + public function doInstall(PackageInterface $package, $path, $url) { SvnUtil::cleanEnv(); $ref = $package->getSourceReference(); diff --git a/src/Composer/Downloader/TarDownloader.php b/src/Composer/Downloader/TarDownloader.php index 34c43da5f..e48407230 100644 --- a/src/Composer/Downloader/TarDownloader.php +++ b/src/Composer/Downloader/TarDownloader.php @@ -12,6 +12,8 @@ namespace Composer\Downloader; +use Composer\Package\PackageInterface; + /** * Downloader for tar files: tar, tar.gz or tar.bz2 * @@ -22,7 +24,7 @@ class TarDownloader extends ArchiveDownloader /** * {@inheritDoc} */ - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { // Can throw an UnexpectedValueException $archive = new \PharData($file); diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index aa666058e..237d7e49d 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -55,6 +55,14 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * {@inheritDoc} */ public function download(PackageInterface $package, $path) + { + // noop for now, ideally we would do a git fetch already here, or make sure the cached git repo is synced, etc. + } + + /** + * {@inheritDoc} + */ + public function install(PackageInterface $package, $path) { if (!$package->getSourceReference()) { throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); @@ -87,7 +95,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa $url = $needle . $url; } } - $this->doDownload($package, $path, $url); + $this->doInstall($package, $path, $url); break; } catch (\Exception $e) { // rethrow phpunit exceptions to avoid hard to debug bug failures @@ -260,7 +268,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @param string $path download path * @param string $url package url */ - abstract protected function doDownload(PackageInterface $package, $path, $url); + abstract protected function doInstall(PackageInterface $package, $path, $url); /** * Updates specific package in specific folder from initial to target version. diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index bd7b028e2..19e51c321 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -37,7 +37,7 @@ class XzDownloader extends ArchiveDownloader parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); } - protected function extract($file, $path) + protected function extract(PackageInterface $package, $file, $path) { $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path); @@ -49,12 +49,4 @@ class XzDownloader extends ArchiveDownloader throw new \RuntimeException($processError); } - - /** - * {@inheritdoc} - */ - protected function getFileName(PackageInterface $package, $path) - { - return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); - } } diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 9eceab250..efa9fc994 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -185,7 +185,7 @@ class ZipDownloader extends ArchiveDownloader * @param string $file File to extract * @param string $path Path where to extract file */ - public function extract($file, $path) + public function extract(PackageInterface $package, $file, $path) { // Each extract calls its alternative if not available or fails if (self::$isWindows) { diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index c96fd2188..e66e64ed1 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -24,6 +24,7 @@ use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; +use Composer\Util\Loop; use Composer\Util\Silencer; use Composer\Plugin\PluginEvents; use Composer\EventDispatcher\Event; @@ -326,6 +327,7 @@ class Factory } $httpDownloader = self::createHttpDownloader($io, $config); + $loop = new Loop($httpDownloader); // initialize event dispatcher $dispatcher = new EventDispatcher($composer, $io); @@ -352,7 +354,7 @@ class Factory $composer->setPackage($package); // initialize installation manager - $im = $this->createInstallationManager(); + $im = $this->createInstallationManager($loop); $composer->setInstallationManager($im); if ($fullLoad) { @@ -365,7 +367,7 @@ class Factory $composer->setAutoloadGenerator($generator); // initialize archive manager - $am = $this->createArchiveManager($config, $dm); + $am = $this->createArchiveManager($config, $dm, $loop); $composer->setArchiveManager($am); } @@ -501,9 +503,9 @@ class Factory * @param Downloader\DownloadManager $dm Manager use to download sources * @return Archiver\ArchiveManager */ - public function createArchiveManager(Config $config, Downloader\DownloadManager $dm) + public function createArchiveManager(Config $config, Downloader\DownloadManager $dm, Loop $loop) { - $am = new Archiver\ArchiveManager($dm); + $am = new Archiver\ArchiveManager($dm, $loop); $am->addArchiver(new Archiver\ZipArchiver); $am->addArchiver(new Archiver\PharArchiver); @@ -525,9 +527,9 @@ class Factory /** * @return Installer\InstallationManager */ - protected function createInstallationManager() + public function createInstallationManager(Loop $loop) { - return new Installer\InstallationManager(); + return new Installer\InstallationManager($loop); } /** diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 9f50b5980..ce10dc4da 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -24,6 +24,7 @@ use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\Util\StreamContextFactory; +use Composer\Util\Loop; /** * Package operation manager. @@ -37,6 +38,12 @@ class InstallationManager private $installers = array(); private $cache = array(); private $notifiablePackages = array(); + private $loop; + + public function __construct(Loop $loop) + { + $this->loop = $loop; + } public function reset() { @@ -156,7 +163,24 @@ class InstallationManager */ public function execute(RepositoryInterface $repo, OperationInterface $operation) { + // TODO this should take all operations in one go $method = $operation->getJobType(); + + if ($method === 'install') { + $package = $operation->getPackage(); + $installer = $this->getInstaller($package->getType()); + $promise = $installer->download($package); + } elseif ($method === 'update') { + $target = $operation->getTargetPackage(); + $targetType = $target->getType(); + $installer = $this->getInstaller($targetType); + $promise = $installer->download($target, $operation->getInitialPackage()); + } + + if (isset($promise)) { + $this->loop->wait(array($promise)); + } + $this->$method($repo, $operation); } @@ -194,7 +218,8 @@ class InstallationManager $this->markForNotification($target); } else { $this->getInstaller($initialType)->uninstall($repo, $initial); - $this->getInstaller($targetType)->install($repo, $target); + $installer = $this->getInstaller($targetType); + $installer->install($repo, $target); } } diff --git a/src/Composer/Installer/InstallerInterface.php b/src/Composer/Installer/InstallerInterface.php index e64dfadd2..e00877ed9 100644 --- a/src/Composer/Installer/InstallerInterface.php +++ b/src/Composer/Installer/InstallerInterface.php @@ -15,6 +15,7 @@ namespace Composer\Installer; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use InvalidArgumentException; +use React\Promise\PromiseInterface; /** * Interface for the package installation manager. @@ -42,6 +43,15 @@ interface InstallerInterface */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package); + /** + * Downloads the files needed to later install the given package. + * + * @param PackageInterface $package package instance + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface + */ + public function download(PackageInterface $package, PackageInterface $prevPackage = null); + /** * Installs specific package. * diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 34fbbbee4..4c2f45601 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -85,6 +85,14 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath); } + public function download(PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->initializeVendorDir(); + $downloadPath = $this->getInstallPath($package); + + return $this->downloadManager->download($package, $downloadPath, $prevPackage); + } + /** * {@inheritDoc} */ @@ -194,7 +202,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface protected function installCode(PackageInterface $package) { $downloadPath = $this->getInstallPath($package); - $this->downloadManager->download($package, $downloadPath); + $this->downloadManager->install($package, $downloadPath); } protected function updateCode(PackageInterface $initial, PackageInterface $target) diff --git a/src/Composer/Installer/MetapackageInstaller.php b/src/Composer/Installer/MetapackageInstaller.php index 3f99ec03c..7dbf4af67 100644 --- a/src/Composer/Installer/MetapackageInstaller.php +++ b/src/Composer/Installer/MetapackageInstaller.php @@ -38,6 +38,14 @@ class MetapackageInstaller implements InstallerInterface return $repo->hasPackage($package); } + /** + * {@inheritDoc} + */ + public function download(PackageInterface $package, PackageInterface $prevPackage = null) + { + // noop + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Installer/NoopInstaller.php b/src/Composer/Installer/NoopInstaller.php index 72cf17d22..51df3c305 100644 --- a/src/Composer/Installer/NoopInstaller.php +++ b/src/Composer/Installer/NoopInstaller.php @@ -40,6 +40,13 @@ class NoopInstaller implements InstallerInterface return $repo->hasPackage($package); } + /** + * {@inheritDoc} + */ + public function download(PackageInterface $package, PackageInterface $prevPackage = null) + { + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index c400ca4a6..62a16fc62 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -50,13 +50,21 @@ class PluginInstaller extends LibraryInstaller /** * {@inheritDoc} */ - public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + public function download(PackageInterface $package, PackageInterface $prevPackage = null) { $extra = $package->getExtra(); if (empty($extra['class'])) { throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); } + return parent::download($package, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { parent::install($repo, $package); try { $this->composer->getPluginManager()->registerPackage($package, true); diff --git a/src/Composer/Installer/ProjectInstaller.php b/src/Composer/Installer/ProjectInstaller.php index c79238b36..350b220f5 100644 --- a/src/Composer/Installer/ProjectInstaller.php +++ b/src/Composer/Installer/ProjectInstaller.php @@ -58,7 +58,7 @@ class ProjectInstaller implements InstallerInterface /** * {@inheritDoc} */ - public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + public function download(PackageInterface $package, PackageInterface $prevPackage = null) { $installPath = $this->installPath; if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) { @@ -67,7 +67,16 @@ class ProjectInstaller implements InstallerInterface if (!is_dir($installPath)) { mkdir($installPath, 0777, true); } - $this->downloadManager->download($package, $installPath); + + return $this->downloadManager->download($package, $installPath, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + $this->downloadManager->install($package, $this->installPath); } /** diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php index 22f8eeafe..359d6b053 100644 --- a/src/Composer/Package/Archiver/ArchiveManager.php +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -16,6 +16,7 @@ use Composer\Downloader\DownloadManager; use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Util\Filesystem; +use Composer\Util\Loop; use Composer\Json\JsonFile; /** @@ -25,6 +26,7 @@ use Composer\Json\JsonFile; class ArchiveManager { protected $downloadManager; + protected $loop; protected $archivers = array(); @@ -36,9 +38,10 @@ class ArchiveManager /** * @param DownloadManager $downloadManager A manager used to download package sources */ - public function __construct(DownloadManager $downloadManager) + public function __construct(DownloadManager $downloadManager, Loop $loop) { $this->downloadManager = $downloadManager; + $this->loop = $loop; } /** @@ -148,7 +151,9 @@ class ArchiveManager $filesystem->ensureDirectoryExists($sourcePath); // Download sources - $this->downloadManager->download($package, $sourcePath); + $promise = $this->downloadManager->download($package, $sourcePath); + $this->loop->wait(array($promise)); + $this->downloadManager->install($package, $sourcePath); // Check exclude from downloaded composer.json if (file_exists($composerJsonPath = $sourcePath.'/composer.json')) { diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 8fe6a04ed..af627df73 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -22,6 +22,7 @@ use Composer\Config; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Util\HttpDownloader; +use Composer\Util\Loop; use Composer\Plugin\PluginEvents; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; @@ -42,6 +43,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $baseUrl; private $io; private $httpDownloader; + private $loop; protected $cache; protected $notifyUrl; protected $searchUrl; @@ -107,6 +109,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->httpDownloader = $httpDownloader; $this->eventDispatcher = $eventDispatcher; $this->repoConfig = $repoConfig; + $this->loop = new Loop($this->httpDownloader); } public function getRepoConfig() @@ -569,6 +572,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->loadRootServerFile(); $packages = array(); + $promises = array(); $repo = $this; if (!$this->lazyProvidersUrl) { @@ -592,7 +596,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $lastModified = isset($contents['last-modified']) ? $contents['last-modified'] : null; } - $this->asyncFetchFile($url, $cacheKey, $lastModified) + $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified) ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) { static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); @@ -637,13 +641,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } } - }, function ($e) { - // TODO use ->done() above instead with react/promise 2.0 - throw $e; }); } - $this->httpDownloader->wait(); + $this->loop->wait($promises); return $packages; // RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed @@ -1119,7 +1120,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $degradedMode = true; - return true; + throw $e; }; return $httpDownloader->add($filename, $options)->then($accept, $reject); diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 3dc6710b6..ff31bf695 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -295,7 +295,7 @@ class CurlDownloader // resolve promise if ($job['filename']) { rename($job['filename'].'~', $job['filename']); - call_user_func($job['resolve'], true); + call_user_func($job['resolve'], $response); } else { call_user_func($job['resolve'], $response); } diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index f2308d75a..172ea875a 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -160,7 +160,10 @@ class HttpDownloader if ($job['request']['copyTo']) { $result = $rfs->copy($job['origin'], $url, $job['request']['copyTo'], false /* TODO progress */, $options); - $resolve($result); + $headers = $rfs->getLastHeaders(); + $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $job['request']['copyTo'].'~'); + + $resolve($response); } else { $body = $rfs->getContents($job['origin'], $url, false /* TODO progress */, $options); $headers = $rfs->getLastHeaders(); @@ -191,6 +194,7 @@ class HttpDownloader $job['exception'] = $e; $downloader->markJobDone(); + $downloader->scheduleNextJob(); throw $e; }); diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php new file mode 100644 index 000000000..1be7d478b --- /dev/null +++ b/src/Composer/Util/Loop.php @@ -0,0 +1,47 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Composer\Util\HttpDownloader; +use React\Promise\Promise; + +/** + * @author Jordi Boggiano + */ +class Loop +{ + private $io; + + public function __construct(HttpDownloader $httpDownloader) + { + $this->httpDownloader = $httpDownloader; + } + + public function wait(array $promises) + { + $uncaught = null; + + \React\Promise\all($promises)->then( + function () { }, + function ($e) use (&$uncaught) { + $uncaught = $e; + } + ); + + $this->httpDownloader->wait(); + + if ($uncaught) { + throw $uncaught; + } + } +} diff --git a/tests/Composer/Test/ComposerTest.php b/tests/Composer/Test/ComposerTest.php index c2c425e76..87270baae 100644 --- a/tests/Composer/Test/ComposerTest.php +++ b/tests/Composer/Test/ComposerTest.php @@ -57,7 +57,7 @@ class ComposerTest extends TestCase public function testSetGetInstallationManager() { $composer = new Composer(); - $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock(); + $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock(); $composer->setInstallationManager($manager); $this->assertSame($manager, $composer->getInstallationManager()); diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php index ddf21c64b..d887ba103 100644 --- a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -29,7 +29,7 @@ class ArchiveDownloaderTest extends TestCase $method->setAccessible(true); $first = $method->invoke($downloader, $packageMock, '/path'); - $this->assertRegExp('#/path/[a-z0-9]+\.js#', $first); + $this->assertRegExp('#/path_[a-z0-9]+\.js#', $first); $this->assertSame($first, $method->invoke($downloader, $packageMock, '/path')); } diff --git a/tests/Composer/Test/Downloader/DownloadManagerTest.php b/tests/Composer/Test/Downloader/DownloadManagerTest.php index 222e541d7..307b2294f 100644 --- a/tests/Composer/Test/Downloader/DownloadManagerTest.php +++ b/tests/Composer/Test/Downloader/DownloadManagerTest.php @@ -50,7 +50,7 @@ class DownloadManagerTest extends TestCase $this->setExpectedException('InvalidArgumentException'); - $manager->getDownloaderForInstalledPackage($package); + $manager->getDownloaderForPackage($package); } public function testGetDownloaderForCorrectlyInstalledDistPackage() @@ -82,7 +82,7 @@ class DownloadManagerTest extends TestCase ->with('pear') ->will($this->returnValue($downloader)); - $this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package)); + $this->assertSame($downloader, $manager->getDownloaderForPackage($package)); } public function testGetDownloaderForIncorrectlyInstalledDistPackage() @@ -116,7 +116,7 @@ class DownloadManagerTest extends TestCase $this->setExpectedException('LogicException'); - $manager->getDownloaderForInstalledPackage($package); + $manager->getDownloaderForPackage($package); } public function testGetDownloaderForCorrectlyInstalledSourcePackage() @@ -148,7 +148,7 @@ class DownloadManagerTest extends TestCase ->with('git') ->will($this->returnValue($downloader)); - $this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package)); + $this->assertSame($downloader, $manager->getDownloaderForPackage($package)); } public function testGetDownloaderForIncorrectlyInstalledSourcePackage() @@ -182,7 +182,7 @@ class DownloadManagerTest extends TestCase $this->setExpectedException('LogicException'); - $manager->getDownloaderForInstalledPackage($package); + $manager->getDownloaderForPackage($package); } public function testGetDownloaderForMetapackage() @@ -195,7 +195,7 @@ class DownloadManagerTest extends TestCase $manager = new DownloadManager($this->io, false, $this->filesystem); - $this->assertNull($manager->getDownloaderForInstalledPackage($package)); + $this->assertNull($manager->getDownloaderForPackage($package)); } public function testFullPackageDownload() @@ -223,11 +223,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -274,16 +274,16 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->at(0)) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloaderFail)); $manager ->expects($this->at(1)) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloaderSuccess)); @@ -333,11 +333,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -369,11 +369,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -399,11 +399,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue(null)); // There is no downloader for Metapackages. @@ -435,11 +435,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -472,11 +472,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -509,11 +509,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -550,33 +550,30 @@ class DownloadManagerTest extends TestCase $initial ->expects($this->once()) ->method('getDistType') - ->will($this->returnValue('pear')); + ->will($this->returnValue('zip')); $target = $this->createPackageMock(); $target ->expects($this->once()) - ->method('getDistType') - ->will($this->returnValue('pear')); + ->method('getInstallationSource') + ->will($this->returnValue('dist')); $target ->expects($this->once()) - ->method('setInstallationSource') - ->with('dist'); + ->method('getDistType') + ->will($this->returnValue('zip')); - $pearDownloader = $this->createDownloaderMock(); - $pearDownloader + $zipDownloader = $this->createDownloaderMock(); + $zipDownloader ->expects($this->once()) ->method('update') ->with($initial, $target, 'vendor/bundles/FOS/UserBundle'); + $zipDownloader + ->expects($this->any()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); - $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) - ->getMock(); - $manager - ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') - ->with($initial) - ->will($this->returnValue($pearDownloader)); + $manager = new DownloadManager($this->io, false, $this->filesystem); + $manager->setDownloader('zip', $zipDownloader); $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle'); } @@ -591,113 +588,89 @@ class DownloadManagerTest extends TestCase $initial ->expects($this->once()) ->method('getDistType') - ->will($this->returnValue('pear')); + ->will($this->returnValue('xz')); $target = $this->createPackageMock(); $target - ->expects($this->once()) + ->expects($this->any()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); + $target + ->expects($this->any()) ->method('getDistType') - ->will($this->returnValue('composer')); + ->will($this->returnValue('zip')); - $pearDownloader = $this->createDownloaderMock(); - $pearDownloader + $xzDownloader = $this->createDownloaderMock(); + $xzDownloader ->expects($this->once()) ->method('remove') ->with($initial, 'vendor/bundles/FOS/UserBundle'); + $xzDownloader + ->expects($this->any()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); - $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage', 'download')) - ->getMock(); - $manager + $zipDownloader = $this->createDownloaderMock(); + $zipDownloader ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') - ->with($initial) - ->will($this->returnValue($pearDownloader)); - $manager - ->expects($this->once()) - ->method('download') - ->with($target, 'vendor/bundles/FOS/UserBundle', false); + ->method('install') + ->with($target, 'vendor/bundles/FOS/UserBundle'); + $zipDownloader + ->expects($this->any()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); + + $manager = new DownloadManager($this->io, false, $this->filesystem); + $manager->setDownloader('xz', $xzDownloader); + $manager->setDownloader('zip', $zipDownloader); $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle'); } - public function testUpdateSourceWithEqualTypes() + /** + * @dataProvider updatesProvider + */ + public function testGetAvailableSourcesUpdateSticksToSameSource($prevPkgSource, $prevPkgIsDev, $targetAvailable, $targetIsDev, $expected) { - $initial = $this->createPackageMock(); - $initial - ->expects($this->once()) - ->method('getInstallationSource') - ->will($this->returnValue('source')); - $initial - ->expects($this->once()) - ->method('getSourceType') - ->will($this->returnValue('svn')); + $initial = null; + if ($prevPkgSource) { + $initial = $this->prophesize('Composer\Package\PackageInterface'); + $initial->getInstallationSource()->willReturn($prevPkgSource); + $initial->isDev()->willReturn($prevPkgIsDev); + } - $target = $this->createPackageMock(); - $target - ->expects($this->once()) - ->method('getSourceType') - ->will($this->returnValue('svn')); + $target = $this->prophesize('Composer\Package\PackageInterface'); + $target->getSourceType()->willReturn(in_array('source', $targetAvailable, true) ? 'git' : null); + $target->getDistType()->willReturn(in_array('dist', $targetAvailable, true) ? 'zip' : null); + $target->isDev()->willReturn($targetIsDev); - $svnDownloader = $this->createDownloaderMock(); - $svnDownloader - ->expects($this->once()) - ->method('update') - ->with($initial, $target, 'vendor/pkg'); - - $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage', 'download')) - ->getMock(); - $manager - ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') - ->with($initial) - ->will($this->returnValue($svnDownloader)); - - $manager->update($initial, $target, 'vendor/pkg'); + $manager = new DownloadManager($this->io, false, $this->filesystem); + $method = new \ReflectionMethod($manager, 'getAvailableSources'); + $method->setAccessible(true); + $this->assertEquals($expected, $method->invoke($manager, $target->reveal(), $initial ? $initial->reveal() : null)); } - public function testUpdateSourceWithNotEqualTypes() + public static function updatesProvider() { - $initial = $this->createPackageMock(); - $initial - ->expects($this->once()) - ->method('getInstallationSource') - ->will($this->returnValue('source')); - $initial - ->expects($this->once()) - ->method('getSourceType') - ->will($this->returnValue('svn')); - - $target = $this->createPackageMock(); - $target - ->expects($this->once()) - ->method('getSourceType') - ->will($this->returnValue('git')); - - $svnDownloader = $this->createDownloaderMock(); - $svnDownloader - ->expects($this->once()) - ->method('remove') - ->with($initial, 'vendor/pkg'); - - $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage', 'download')) - ->getMock(); - $manager - ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') - ->with($initial) - ->will($this->returnValue($svnDownloader)); - $manager - ->expects($this->once()) - ->method('download') - ->with($target, 'vendor/pkg', true); - - $manager->update($initial, $target, 'vendor/pkg'); + return array( + // prevPkg source, prevPkg isDev, pkg available, pkg isDev, expected + // updates keep previous source as preference + array('source', false, array('source', 'dist'), false, array('source', 'dist')), + array('dist', false, array('source', 'dist'), false, array('dist', 'source')), + // updates do not keep previous source if target package does not have it + array('source', false, array('dist'), false, array('dist')), + array('dist', false, array('source'), false, array('source')), + // updates do not keep previous source if target is dev and prev wasn't dev and installed from dist + array('source', false, array('source', 'dist'), true, array('source', 'dist')), + array('dist', false, array('source', 'dist'), true, array('source', 'dist')), + // install picks the right default + array(null, null, array('source', 'dist'), true, array('source', 'dist')), + array(null, null, array('dist'), true, array('dist')), + array(null, null, array('source'), true, array('source')), + array(null, null, array('source', 'dist'), false, array('dist', 'source')), + array(null, null, array('dist'), false, array('dist')), + array(null, null, array('source'), false, array('source')), + ); } public function testUpdateMetapackage() @@ -707,11 +680,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager - ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->expects($this->exactly(2)) + ->method('getDownloaderForPackage') ->with($initial) ->will($this->returnValue(null)); // There is no downloader for metapackages. @@ -730,11 +703,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($pearDownloader)); @@ -747,11 +720,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue(null)); // There is no downloader for metapackages. @@ -790,11 +763,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -833,11 +806,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); @@ -879,11 +852,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'source')); @@ -926,11 +899,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'source')); @@ -973,11 +946,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'auto')); @@ -1020,11 +993,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'auto')); @@ -1063,11 +1036,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'source')); @@ -1106,11 +1079,11 @@ class DownloadManagerTest extends TestCase $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->setConstructorArgs(array($this->io, false, $this->filesystem)) - ->setMethods(array('getDownloaderForInstalledPackage')) + ->setMethods(array('getDownloaderForPackage')) ->getMock(); $manager ->expects($this->once()) - ->method('getDownloaderForInstalledPackage') + ->method('getDownloaderForPackage') ->with($package) ->will($this->returnValue($downloader)); $manager->setPreferences(array('foo/*' => 'dist')); diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index 12edfe19d..9ce536474 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -15,6 +15,8 @@ namespace Composer\Test\Downloader; use Composer\Downloader\FileDownloader; use Composer\Test\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; +use Composer\Util\Loop; class FileDownloaderTest extends TestCase { @@ -23,6 +25,11 @@ class FileDownloaderTest extends TestCase $io = $io ?: $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $config = $config ?: $this->getMockBuilder('Composer\Config')->getMock(); $httpDownloader = $httpDownloader ?: $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); + $httpDownloader + ->expects($this->any()) + ->method('addCopy') + ->will($this->returnValue(\React\Promise\resolve(new Response(array('url' => 'http://example.org/'), 200, array(), 'file~')))); + $this->httpDownloader = $httpDownloader; return new FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $filesystem); } @@ -84,7 +91,7 @@ class FileDownloaderTest extends TestCase $method = new \ReflectionMethod($downloader, 'getFileName'); $method->setAccessible(true); - $this->assertEquals('/path/script.js', $method->invoke($downloader, $packageMock, '/path')); + $this->assertEquals('/path_script.js', $method->invoke($downloader, $packageMock, '/path')); } public function testDownloadButFileIsUnsaved() @@ -118,8 +125,11 @@ class FileDownloaderTest extends TestCase $downloader = $this->getDownloader($ioMock); try { - $downloader->download($packageMock, $path); - $this->fail(); + $promise = $downloader->download($packageMock, $path); + $loop = new Loop($this->httpDownloader); + $loop->wait(array($promise)); + + $this->fail('Download was expected to throw'); } catch (\Exception $e) { if (is_dir($path)) { $fs = new Filesystem(); @@ -128,7 +138,7 @@ class FileDownloaderTest extends TestCase unlink($path); } - $this->assertInstanceOf('UnexpectedValueException', $e); + $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); $this->assertContains('could not be saved to', $e->getMessage()); } } @@ -188,11 +198,14 @@ class FileDownloaderTest extends TestCase $path = $this->getUniqueTmpDirectory(); $downloader = $this->getDownloader(null, null, null, null, null, $filesystem); // make sure the file expected to be downloaded is on disk already - touch($path.'/script.js'); + touch($path.'_script.js'); try { - $downloader->download($packageMock, $path); - $this->fail(); + $promise = $downloader->download($packageMock, $path); + $loop = new Loop($this->httpDownloader); + $loop->wait(array($promise)); + + $this->fail('Download was expected to throw'); } catch (\Exception $e) { if (is_dir($path)) { $fs = new Filesystem(); @@ -201,7 +214,7 @@ class FileDownloaderTest extends TestCase unlink($path); } - $this->assertInstanceOf('UnexpectedValueException', $e); + $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); $this->assertContains('checksum verification', $e->getMessage()); } } @@ -232,17 +245,25 @@ class FileDownloaderTest extends TestCase $ioMock = $this->getMock('Composer\IO\IOInterface'); $ioMock->expects($this->at(0)) + ->method('writeError') + ->with($this->stringContains('Downloading')); + + $ioMock->expects($this->at(1)) ->method('writeError') ->with($this->stringContains('Downgrading')); $path = $this->getUniqueTmpDirectory(); - touch($path.'/script.js'); + touch($path.'_script.js'); $filesystem = $this->getMock('Composer\Util\Filesystem'); $filesystem->expects($this->once()) ->method('removeDirectory') ->will($this->returnValue(true)); $downloader = $this->getDownloader($ioMock, null, null, null, null, $filesystem); + $promise = $downloader->download($newPackage, $path, $oldPackage); + $loop = new Loop($this->httpDownloader); + $loop->wait(array($promise)); + $downloader->update($oldPackage, $newPackage, $path); } } diff --git a/tests/Composer/Test/Downloader/FossilDownloaderTest.php b/tests/Composer/Test/Downloader/FossilDownloaderTest.php index 623f7dec2..9ab7b6b84 100644 --- a/tests/Composer/Test/Downloader/FossilDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FossilDownloaderTest.php @@ -56,7 +56,7 @@ class FossilDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); - $downloader->download($packageMock, '/path'); + $downloader->install($packageMock, '/path'); } public function testDownload() @@ -89,7 +89,7 @@ class FossilDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->download($packageMock, 'repo'); + $downloader->install($packageMock, 'repo'); } /** diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index c3cd31a4a..b5d0054de 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -79,7 +79,7 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); - $downloader->download($packageMock, '/path'); + $downloader->install($packageMock, '/path'); } public function testDownload() @@ -130,7 +130,7 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->download($packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); } public function testDownloadWithCache() @@ -195,7 +195,7 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, $config, $processExecutor); - $downloader->download($packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); @rmdir($cachePath); } @@ -265,7 +265,7 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); - $downloader->download($packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); } public function pushUrlProvider() @@ -329,7 +329,7 @@ class GitDownloaderTest extends TestCase $config->merge(array('config' => array('github-protocols' => $protocols))); $downloader = $this->getDownloaderMock(null, $config, $processExecutor); - $downloader->download($packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); } /** @@ -360,7 +360,7 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(1)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->download($packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); } /** diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php index c71d463cb..a4219d143 100644 --- a/tests/Composer/Test/Downloader/HgDownloaderTest.php +++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php @@ -56,7 +56,7 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); - $downloader->download($packageMock, '/path'); + $downloader->install($packageMock, '/path'); } public function testDownload() @@ -83,7 +83,7 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->download($packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); } /** diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php index 1b5041d9f..d2b8fb753 100644 --- a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php +++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php @@ -138,7 +138,7 @@ class PerforceDownloaderTest extends TestCase $perforce->expects($this->at(5))->method('syncCodeBase')->with($label); $perforce->expects($this->at(6))->method('cleanupClientSpec'); $this->downloader->setPerforce($perforce); - $this->downloader->doDownload($this->package, $this->testPath, 'url'); + $this->downloader->doInstall($this->package, $this->testPath, 'url'); } /** @@ -161,6 +161,6 @@ class PerforceDownloaderTest extends TestCase $perforce->expects($this->at(5))->method('syncCodeBase')->with($label); $perforce->expects($this->at(6))->method('cleanupClientSpec'); $this->downloader->setPerforce($perforce); - $this->downloader->doDownload($this->package, $this->testPath, 'url'); + $this->downloader->doInstall($this->package, $this->testPath, 'url'); } } diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index 451592d37..4c2fdb2af 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -16,6 +16,7 @@ use Composer\Downloader\XzDownloader; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Composer\Util\Platform; +use Composer\Util\Loop; use Composer\Util\HttpDownloader; class XzDownloaderTest extends TestCase @@ -66,10 +67,14 @@ class XzDownloaderTest extends TestCase ->method('get') ->with('vendor-dir') ->will($this->returnValue($this->testDir)); - $downloader = new XzDownloader($io, $config, new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null); + $downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null); try { - $downloader->download($packageMock, $this->getUniqueTmpDirectory()); + $promise = $downloader->download($packageMock, $this->testDir); + $loop = new Loop($httpDownloader); + $loop->wait(array($promise)); + $downloader->install($packageMock, $this->testDir); + $this->fail('Download of invalid tarball should throw an exception'); } catch (\RuntimeException $e) { $this->assertRegexp('/(File format not recognized|Unrecognized archive format)/i', $e->getMessage()); diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 0c1311427..b754af607 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -17,6 +17,7 @@ use Composer\Package\PackageInterface; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Composer\Util\HttpDownloader; +use Composer\Util\Loop; class ZipDownloaderTest extends TestCase { @@ -27,6 +28,7 @@ class ZipDownloaderTest extends TestCase private $prophet; private $io; private $config; + private $package; public function setUp() { @@ -35,6 +37,7 @@ class ZipDownloaderTest extends TestCase $this->config = $this->getMockBuilder('Composer\Config')->getMock(); $dlConfig = $this->getMockBuilder('Composer\Config')->getMock(); $this->httpDownloader = new HttpDownloader($this->io, $dlConfig); + $this->package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); } public function tearDown() @@ -71,16 +74,15 @@ class ZipDownloaderTest extends TestCase ->with('vendor-dir') ->will($this->returnValue($this->testDir)); - $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); - $packageMock->expects($this->any()) + $this->package->expects($this->any()) ->method('getDistUrl') ->will($this->returnValue($distUrl = 'file://'.__FILE__)) ; - $packageMock->expects($this->any()) + $this->package->expects($this->any()) ->method('getDistUrls') ->will($this->returnValue(array($distUrl))) ; - $packageMock->expects($this->atLeastOnce()) + $this->package->expects($this->atLeastOnce()) ->method('getTransportOptions') ->will($this->returnValue(array())) ; @@ -90,7 +92,11 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', false); try { - $downloader->download($packageMock, sys_get_temp_dir().'/composer-zip-test'); + $promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test'); + $loop = new Loop($this->httpDownloader); + $loop->wait(array($promise)); + $downloader->install($this->package, $path); + $this->fail('Download of invalid zip files should throw an exception'); } catch (\Exception $e) { $this->assertContains('is not a zip archive', $e->getMessage()); @@ -119,7 +125,7 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(false)); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -144,7 +150,7 @@ class ZipDownloaderTest extends TestCase ->will($this->throwException(new \ErrorException('Not a directory'))); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -168,7 +174,7 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(true)); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -189,7 +195,7 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(1)); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } public function testSystemUnzipOnlyGood() @@ -206,7 +212,7 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } public function testNonWindowsFallbackGood() @@ -234,7 +240,7 @@ class ZipDownloaderTest extends TestCase $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -266,7 +272,7 @@ class ZipDownloaderTest extends TestCase $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } public function testWindowsFallbackGood() @@ -294,7 +300,7 @@ class ZipDownloaderTest extends TestCase $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } /** @@ -326,7 +332,7 @@ class ZipDownloaderTest extends TestCase $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract('testfile.zip', 'vendor/dir'); + $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } } @@ -337,8 +343,13 @@ class MockedZipDownloader extends ZipDownloader return; } - public function extract($file, $path) + public function install(PackageInterface $package, $path, $output = true) { - parent::extract($file, $path); + return; + } + + public function extract(PackageInterface $package, $file, $path) + { + parent::extract($package, $file, $path); } } diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 7786e7807..6d812e20a 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -101,7 +101,7 @@ class EventDispatcherTest extends TestCase $composer->setPackage($package); $composer->setRepositoryManager($this->getRepositoryManagerMockForDevModePassingTest()); - $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->getMock()); + $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock()); $dispatcher = new EventDispatcher( $composer, diff --git a/tests/Composer/Test/Installer/InstallationManagerTest.php b/tests/Composer/Test/Installer/InstallationManagerTest.php index 86e860bc2..407a5b10f 100644 --- a/tests/Composer/Test/Installer/InstallationManagerTest.php +++ b/tests/Composer/Test/Installer/InstallationManagerTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Installer; use Composer\Installer\InstallationManager; +use Composer\Installer\NoopInstaller; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -21,9 +22,11 @@ use PHPUnit\Framework\TestCase; class InstallationManagerTest extends TestCase { protected $repository; + protected $loop; public function setUp() { + $this->loop = $this->getMockBuilder('Composer\Util\Loop')->disableOriginalConstructor()->getMock(); $this->repository = $this->getMockBuilder('Composer\Repository\InstalledRepositoryInterface')->getMock(); } @@ -38,7 +41,7 @@ class InstallationManagerTest extends TestCase return $arg === 'vendor'; })); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop); $manager->addInstaller($installer); $this->assertSame($installer, $manager->getInstaller('vendor')); @@ -67,7 +70,7 @@ class InstallationManagerTest extends TestCase return $arg === 'vendor'; })); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop); $manager->addInstaller($installer); $this->assertSame($installer, $manager->getInstaller('vendor')); @@ -80,16 +83,21 @@ class InstallationManagerTest extends TestCase public function testExecute() { $manager = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->setConstructorArgs(array($this->loop)) ->setMethods(array('install', 'update', 'uninstall')) ->getMock(); - $installOperation = new InstallOperation($this->createPackageMock()); - $removeOperation = new UninstallOperation($this->createPackageMock()); + $installOperation = new InstallOperation($package = $this->createPackageMock()); + $removeOperation = new UninstallOperation($package); $updateOperation = new UpdateOperation( - $this->createPackageMock(), - $this->createPackageMock() + $package, + $package ); + $package->expects($this->any()) + ->method('getType') + ->will($this->returnValue('library')); + $manager ->expects($this->once()) ->method('install') @@ -103,6 +111,7 @@ class InstallationManagerTest extends TestCase ->method('update') ->with($this->repository, $updateOperation); + $manager->addInstaller(new NoopInstaller()); $manager->execute($this->repository, $installOperation); $manager->execute($this->repository, $removeOperation); $manager->execute($this->repository, $updateOperation); @@ -111,7 +120,7 @@ class InstallationManagerTest extends TestCase public function testInstall() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop); $manager->addInstaller($installer); $package = $this->createPackageMock(); @@ -139,7 +148,7 @@ class InstallationManagerTest extends TestCase public function testUpdateWithEqualTypes() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop); $manager->addInstaller($installer); $initial = $this->createPackageMock(); @@ -173,18 +182,17 @@ class InstallationManagerTest extends TestCase { $libInstaller = $this->createInstallerMock(); $bundleInstaller = $this->createInstallerMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop); $manager->addInstaller($libInstaller); $manager->addInstaller($bundleInstaller); $initial = $this->createPackageMock(); - $target = $this->createPackageMock(); - $operation = new UpdateOperation($initial, $target, 'test'); - $initial ->expects($this->once()) ->method('getType') ->will($this->returnValue('library')); + + $target = $this->createPackageMock(); $target ->expects($this->once()) ->method('getType') @@ -213,13 +221,14 @@ class InstallationManagerTest extends TestCase ->method('install') ->with($this->repository, $target); + $operation = new UpdateOperation($initial, $target, 'test'); $manager->update($this->repository, $operation); } public function testUninstall() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop); $manager->addInstaller($installer); $package = $this->createPackageMock(); @@ -249,7 +258,7 @@ class InstallationManagerTest extends TestCase $installer = $this->getMockBuilder('Composer\Installer\LibraryInstaller') ->disableOriginalConstructor() ->getMock(); - $manager = new InstallationManager(); + $manager = new InstallationManager($this->loop); $manager->addInstaller($installer); $package = $this->createPackageMock(); @@ -281,7 +290,9 @@ class InstallationManagerTest extends TestCase private function createPackageMock() { - return $this->getMockBuilder('Composer\Package\PackageInterface') + $mock = $this->getMockBuilder('Composer\Package\PackageInterface') ->getMock(); + + return $mock; } } diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index 772bb05c8..672f8eb0a 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -113,7 +113,7 @@ class LibraryInstallerTest extends TestCase $this->dm ->expects($this->once()) - ->method('download') + ->method('install') ->with($package, $this->vendorDir.'/some/package'); $this->repository diff --git a/tests/Composer/Test/Mock/FactoryMock.php b/tests/Composer/Test/Mock/FactoryMock.php index 47683afcd..fcb93d2cc 100644 --- a/tests/Composer/Test/Mock/FactoryMock.php +++ b/tests/Composer/Test/Mock/FactoryMock.php @@ -20,6 +20,7 @@ use Composer\Repository\WritableRepositoryInterface; use Composer\Installer; use Composer\IO\IOInterface; use Composer\Test\TestCase; +use Composer\Util\Loop; class FactoryMock extends Factory { @@ -39,9 +40,9 @@ class FactoryMock extends Factory { } - protected function createInstallationManager() + public function createInstallationManager(Loop $loop) { - return new InstallationManagerMock; + return new InstallationManagerMock(); } protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io) diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index de1de514b..21e717224 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -17,6 +17,7 @@ use Composer\Repository\RepositoryInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation; @@ -29,6 +30,18 @@ class InstallationManagerMock extends InstallationManager private $uninstalled = array(); private $trace = array(); + public function __construct() + { + + } + + public function execute(RepositoryInterface $repo, OperationInterface $operation) + { + $method = $operation->getJobType(); + // skipping download() step here for tests + $this->$method($repo, $operation); + } + public function getInstallPath(PackageInterface $package) { return ''; diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index b9f08e693..714c9b923 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -16,6 +16,7 @@ use Composer\IO\NullIO; use Composer\Factory; use Composer\Package\Archiver\ArchiveManager; use Composer\Package\PackageInterface; +use Composer\Util\Loop; use Composer\Test\Mock\FactoryMock; class ArchiveManagerTest extends ArchiverTest @@ -35,9 +36,10 @@ class ArchiveManagerTest extends ArchiverTest $dm = $factory->createDownloadManager( $io = new NullIO, $config = FactoryMock::createConfig(), - $factory->createHttpDownloader($io, $config) + $httpDownloader = $factory->createHttpDownloader($io, $config) ); - $this->manager = $factory->createArchiveManager($factory->createConfig(), $dm); + $loop = new Loop($httpDownloader); + $this->manager = $factory->createArchiveManager($factory->createConfig(), $dm, $loop); $this->targetDir = $this->testDir.'/composer_archiver_tests'; } diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index 01832f94d..633c5ab18 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -89,7 +89,7 @@ class PluginInstallerTest extends TestCase ->method('getLocalRepository') ->will($this->returnValue($this->repository)); - $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock(); + $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock(); $im->expects($this->any()) ->method('getInstallPath') ->will($this->returnCallback(function ($package) { From ea978ee7f8e4897667c4b490c839145582a33cd1 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 18 Jan 2019 08:46:46 +0100 Subject: [PATCH 054/321] Update react/promise requirement --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1b75131bc..03839772e 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", "symfony/finder": "^2.7 || ^3.0 || ^4.0", "symfony/process": "^2.7 || ^3.0 || ^4.0", - "react/promise": "^1.2" + "react/promise": "^1.2 || ^2.7" }, "conflict": { "symfony/console": "2.8.38" diff --git a/composer.lock b/composer.lock index a22e11a6b..d2a448608 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3243ce6f26231df34d1bceab1a148803", + "content-hash": "b078b12b2912d599e0c6904f64def484", "packages": [ { "name": "composer/ca-bundle", From 549ccd8f794c0c1c51b957e864cc914482e82a85 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 18 Jan 2019 11:48:51 +0100 Subject: [PATCH 055/321] Remote outputProgress concept from downloaders as it does not make sense when things happen in parallel, refs #7901 --- src/Composer/Command/CreateProjectCommand.php | 4 +--- src/Composer/Command/InstallCommand.php | 1 - src/Composer/Command/RemoveCommand.php | 1 - src/Composer/Command/RequireCommand.php | 1 - src/Composer/Command/UpdateCommand.php | 2 -- src/Composer/Downloader/DownloadManager.php | 16 ---------------- src/Composer/Downloader/DownloaderInterface.php | 8 -------- src/Composer/Downloader/FileDownloader.php | 14 -------------- src/Composer/Downloader/VcsDownloader.php | 9 --------- 9 files changed, 1 insertion(+), 55 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index e165649a6..c11a0595e 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -162,7 +162,6 @@ EOT } $composer = Factory::create($io, null, $disablePlugins); - $composer->getDownloadManager()->setOutputProgress(!$noProgress); $fs = new Filesystem(); @@ -351,8 +350,7 @@ EOT $httpDownloader = $factory->createHttpDownloader($io, $config); $dm = $factory->createDownloadManager($io, $config, $httpDownloader); $dm->setPreferSource($preferSource) - ->setPreferDist($preferDist) - ->setOutputProgress(!$noProgress); + ->setPreferDist($preferDist); $projectInstaller = new ProjectInstaller($directory, $dm); $im = $factory->createInstallationManager(new Loop($httpDownloader)); diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index cc590d8c9..951d20289 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -85,7 +85,6 @@ EOT } $composer = $this->getComposer(true, $input->getOption('no-plugins')); - $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 27be1a0ca..ea412ec66 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -126,7 +126,6 @@ EOT // Update packages $this->resetComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins')); - $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 1f29751b9..f15308bad 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -167,7 +167,6 @@ EOT // Update packages $this->resetComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins')); - $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 34420b747..06f998d63 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -120,8 +120,6 @@ EOT } } - $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); - $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index 4bc865827..0b1ddb5a6 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -85,22 +85,6 @@ class DownloadManager return $this; } - /** - * Sets whether to output download progress information for all registered - * downloaders - * - * @param bool $outputProgress - * @return DownloadManager - */ - public function setOutputProgress($outputProgress) - { - foreach ($this->downloaders as $downloader) { - $downloader->setOutputProgress($outputProgress); - } - - return $this; - } - /** * Sets installer downloader for a specific installation type. * diff --git a/src/Composer/Downloader/DownloaderInterface.php b/src/Composer/Downloader/DownloaderInterface.php index ac56583c4..2074b16da 100644 --- a/src/Composer/Downloader/DownloaderInterface.php +++ b/src/Composer/Downloader/DownloaderInterface.php @@ -61,12 +61,4 @@ interface DownloaderInterface * @param string $path download path */ public function remove(PackageInterface $package, $path); - - /** - * Sets whether to output download progress information or not - * - * @param bool $outputProgress - * @return DownloaderInterface - */ - public function setOutputProgress($outputProgress); } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 3418eef84..54acae710 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -43,7 +43,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface protected $httpDownloader; protected $filesystem; protected $cache; - protected $outputProgress = true; /** * @private this is only public for php 5.3 support in closures */ @@ -236,16 +235,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $this->filesystem->rename($this->getFileName($package, $path), $path . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME)); } - /** - * {@inheritDoc} - */ - public function setOutputProgress($outputProgress) - { - $this->outputProgress = $outputProgress; - - return $this; - } - /** * TODO mark private in v3 * @protected This is public due to PHP 5.3 @@ -340,11 +329,9 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface public function getLocalChanges(PackageInterface $package, $targetDir) { $prevIO = $this->io; - $prevProgress = $this->outputProgress; $this->io = new NullIO; $this->io->loadConfiguration($this->config); - $this->outputProgress = false; $e = null; try { @@ -362,7 +349,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface } $this->io = $prevIO; - $this->outputProgress = $prevProgress; if ($e) { throw $e; diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index 237d7e49d..b87f6433a 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -210,15 +210,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa } } - /** - * Download progress information is not available for all VCS downloaders. - * {@inheritDoc} - */ - public function setOutputProgress($outputProgress) - { - return $this; - } - /** * {@inheritDoc} */ From bb2f64c7bc37bbc01de6eb4efa49719c5ff80a77 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 18 Jan 2019 12:14:37 +0100 Subject: [PATCH 056/321] Remove ability to override the entire HttpDownloader instance in PRE_FILE_DOWNLOAD events --- src/Composer/Downloader/FileDownloader.php | 6 ++---- src/Composer/Plugin/PreFileDownloadEvent.php | 18 +++++------------- src/Composer/Repository/ComposerRepository.php | 13 +++---------- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 54acae710..4f64a9501 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -106,21 +106,19 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $io = $this->io; $cache = $this->cache; - $originalHttpDownloader = $this->httpDownloader; + $httpDownloader = $this->httpDownloader; $eventDispatcher = $this->eventDispatcher; $filesystem = $this->filesystem; $self = $this; $accept = null; $reject = null; - $download = function () use ($io, $output, $originalHttpDownloader, $cache, $eventDispatcher, $package, $fileName, $path, &$urls, &$accept, &$reject) { + $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, $path, &$urls, &$accept, &$reject) { $url = reset($urls); - $httpDownloader = $originalHttpDownloader; if ($eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']); $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); } $checksum = $package->getDistSha1Checksum(); diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 076449484..c2751da02 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -25,7 +25,7 @@ class PreFileDownloadEvent extends Event /** * @var HttpDownloader */ - private $rfs; + private $httpDownloader; /** * @var string @@ -36,13 +36,13 @@ class PreFileDownloadEvent extends Event * Constructor. * * @param string $name The event name - * @param HttpDownloader $rfs + * @param HttpDownloader $httpDownloader * @param string $processedUrl */ - public function __construct($name, HttpDownloader $rfs, $processedUrl) + public function __construct($name, HttpDownloader $httpDownloader, $processedUrl) { parent::__construct($name); - $this->rfs = $rfs; + $this->httpDownloader = $httpDownloader; $this->processedUrl = $processedUrl; } @@ -51,15 +51,7 @@ class PreFileDownloadEvent extends Event */ public function getHttpDownloader() { - return $this->rfs; - } - - /** - * @param HttpDownloader $rfs - */ - public function setHttpDownloader(HttpDownloader $rfs) - { - $this->rfs = $rfs; + return $this->httpDownloader; } /** diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index af627df73..e905f2b22 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -898,15 +898,12 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; while ($retries--) { try { - $httpDownloader = $this->httpDownloader; - if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); } - $response = $httpDownloader->get($filename, $this->options); + $response = $this->httpDownloader->get($filename, $this->options); $json = $response->getBody(); if ($sha256 && $sha256 !== hash('sha256', $json)) { // undo downgrade before trying again if http seems to be hijacked or modifying content somehow @@ -989,12 +986,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; while ($retries--) { try { - $httpDownloader = $this->httpDownloader; - if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); } $options = $this->options; @@ -1002,7 +996,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $options['http']['header'] = (array) $options['http']['header']; } $options['http']['header'][] = array('If-Modified-Since: '.$lastModifiedTime); - $response = $httpDownloader->get($filename, $options); + $response = $this->httpDownloader->get($filename, $options); $json = $response->getBody(); if ($json === '' && $response->getStatusCode() === 304) { return true; @@ -1053,12 +1047,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private function asyncFetchFile($filename, $cacheKey, $lastModifiedTime = null) { $retries = 3; - $httpDownloader = $this->httpDownloader; + $httpDownloader = $this->httpDownloader; if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); - $httpDownloader = $preFileDownloadEvent->getHttpDownloader(); } $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); From f54237159da2437aea09a0b70a8214c7766527a0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 18 Jan 2019 12:14:47 +0100 Subject: [PATCH 057/321] Cleanups --- tests/Composer/Test/Repository/Pear/ChannelReaderTest.php | 8 ++++---- .../Test/Repository/Pear/ChannelRest10ReaderTest.php | 4 ++-- .../Test/Repository/Pear/ChannelRest11ReaderTest.php | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php index 7ad30825d..95e59e906 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php @@ -28,13 +28,13 @@ class ChannelReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new HttpDownloaderMock(array( + $httpDownloader = new HttpDownloaderMock(array( 'http://pear.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'), 'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'), 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), )); - $reader = new \Composer\Repository\Pear\ChannelReader($rfs); + $reader = new \Composer\Repository\Pear\ChannelReader($httpDownloader); $channelInfo = $reader->read('http://pear.net/'); $packages = $channelInfo->getPackages(); @@ -50,7 +50,7 @@ class ChannelReaderTest extends TestCase public function testShouldSelectCorrectReader() { - $rfs = new HttpDownloaderMock(array( + $httpDownloader = new HttpDownloaderMock(array( 'http://pear.1.0.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.0.xml'), 'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'), 'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'), @@ -64,7 +64,7 @@ class ChannelReaderTest extends TestCase 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), )); - $reader = new \Composer\Repository\Pear\ChannelReader($rfs); + $reader = new \Composer\Repository\Pear\ChannelReader($httpDownloader); $reader->read('http://pear.1.0.net/'); $reader->read('http://pear.1.1.net/'); diff --git a/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php index 5a40915e1..3960c7858 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php @@ -19,7 +19,7 @@ class ChannelRest10ReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new HttpDownloaderMock(array( + $httpDownloader = new HttpDownloaderMock(array( 'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'), 'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'), 'http://test.loc/rest10/r/http_client/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_allreleases.xml'), @@ -29,7 +29,7 @@ class ChannelRest10ReaderTest extends TestCase 'http://test.loc/rest10/r/http_request/deps.1.4.0.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_deps.1.4.0.txt'), )); - $reader = new \Composer\Repository\Pear\ChannelRest10Reader($rfs); + $reader = new \Composer\Repository\Pear\ChannelRest10Reader($httpDownloader); /** @var \Composer\Package\PackageInterface[] $packages */ $packages = $reader->read('http://test.loc/rest10'); diff --git a/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php index 08c3a2998..684c59155 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php @@ -19,13 +19,13 @@ class ChannelRest11ReaderTest extends TestCase { public function testShouldBuildPackagesFromPearSchema() { - $rfs = new HttpDownloaderMock(array( + $httpDownloader = new HttpDownloaderMock(array( 'http://pear.1.1.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'), 'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'), 'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'), )); - $reader = new \Composer\Repository\Pear\ChannelRest11Reader($rfs); + $reader = new \Composer\Repository\Pear\ChannelRest11Reader($httpDownloader); /** @var \Composer\Package\PackageInterface[] $packages */ $packages = $reader->read('http://test.loc/rest11'); From 37550ce44b5c7d8f42c3528389a354af4b975d59 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 18 Jan 2019 18:49:45 +0100 Subject: [PATCH 058/321] Add support for new minified format --- .../Repository/ComposerRepository.php | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e905f2b22..bb613497f 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -598,8 +598,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified) ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) { - static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); - if (true === $response) { $response = $contents; } @@ -608,8 +606,38 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return; } + $versions = $response['packages'][$name]; + + if (isset($response['minified']) && $response['minified'] === 'composer/2.0') { + // TODO extract in other method + $expanded = array(); + $expandedVersion = null; + foreach ($versions as $versionData) { + if (!$expandedVersion) { + $expandedVersion = $versionData; + $expanded[] = $expandedVersion; + continue; + } + + // add any changes from the previous version to the expanded one + foreach ($versionData as $key => $val) { + if ($val === '__unset') { + unset($expandedVersion[$key]); + } else { + $expandedVersion[$key] = $val; + } + } + + $expanded[] = $expandedVersion; + } + + $versions = $expanded; + unset($expanded, $expandedVersion, $versionData); + } + + static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); $versionsToLoad = array(); - foreach ($response['packages'][$name] as $version) { + foreach ($versions as $version) { if (isset($version['version_normalizeds'])) { foreach ($version['version_normalizeds'] as $index => $normalizedVersion) { if (!$repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $normalizedVersion)) { From 71193132a3d7de69bb42e01d4a1524f6069d4a55 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 29 Jan 2019 14:22:20 +0100 Subject: [PATCH 059/321] Fix test --- tests/Composer/Test/Downloader/FileDownloaderTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index f3f5dc6dd..10ea401a8 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -222,7 +222,7 @@ class FileDownloaderTest extends TestCase public function testDowngradeShowsAppropriateMessage() { $oldPackage = $this->getMock('Composer\Package\PackageInterface'); - $oldPackage->expects($this->once()) + $oldPackage->expects($this->any()) ->method('getFullPrettyVersion') ->will($this->returnValue('1.2.0')); $oldPackage->expects($this->once()) @@ -230,7 +230,7 @@ class FileDownloaderTest extends TestCase ->will($this->returnValue('1.2.0.0')); $newPackage = $this->getMock('Composer\Package\PackageInterface'); - $newPackage->expects($this->once()) + $newPackage->expects($this->any()) ->method('getFullPrettyVersion') ->will($this->returnValue('1.0.0')); $newPackage->expects($this->once()) From 1211ba1d5177cf17a635fb8eb0f16db13fdf31a4 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Mon, 12 Nov 2018 19:45:10 +0100 Subject: [PATCH 060/321] BC break: Remove workaround for loading lock files without dev requires --- src/Composer/Installer.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 19c0015d6..10f115daf 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -350,16 +350,7 @@ class Installer // and a lock file is present as we need to force install non-whitelisted lock file // packages in that case if (!$this->update || (!empty($this->updateWhitelist) && $this->locker->isLocked())) { - try { - $lockedRepository = $this->locker->getLockedRepository($this->devMode); - } catch (\RuntimeException $e) { - // if there are dev requires, then we really can not install - if ($this->package->getDevRequires()) { - throw $e; - } - // no require-dev in composer.json and the lock file was created with no dev info, so skip them - $lockedRepository = $this->locker->getLockedRepository(); - } + $lockedRepository = $this->locker->getLockedRepository($this->devMode); } $this->whitelistUpdateDependencies( From 10ada7bf82d317bb18813e62b7b103a0f5a86880 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 13 Sep 2018 15:23:05 +0200 Subject: [PATCH 061/321] Refactor Installer class into separate install and update processes - Introduce separate Lock and LocalRepo transactions, one for changes to the lock file, one for changes to locally installed packages based on lock file - Remove various hacks to keep dev dependencies updated and incorporated the functionality into the transaction classes - Remove installed repo, there are now local repo, locked repo and platform repo - Remove access to local repo from solver, only supply locked packages - Update can now be run to modify the lock file but not install packages to local repo --- .../DependencyResolver/DefaultPolicy.php | 54 +- .../LocalRepoTransaction.php | 185 ++++ .../DependencyResolver/LockTransaction.php | 167 +++ .../DependencyResolver/PolicyInterface.php | 4 +- src/Composer/DependencyResolver/Pool.php | 7 - .../DependencyResolver/PoolBuilder.php | 6 +- src/Composer/DependencyResolver/Problem.php | 2 +- src/Composer/DependencyResolver/Request.php | 80 +- src/Composer/DependencyResolver/Rule.php | 2 +- .../DependencyResolver/RuleSetGenerator.php | 37 +- src/Composer/DependencyResolver/Solver.php | 87 +- .../DependencyResolver/Transaction.php | 244 ----- .../EventDispatcher/EventDispatcher.php | 5 +- src/Composer/Installer.php | 992 ++++++------------ .../Installer/SuggestedPackagesReporter.php | 14 +- src/Composer/Package/Locker.php | 23 +- .../DependencyResolver/DefaultPolicyTest.php | 54 +- .../Test/DependencyResolver/RequestTest.php | 20 +- .../Test/DependencyResolver/SolverTest.php | 5 +- .../installer/update-changes-url.test | 49 + tests/Composer/Test/InstallerTest.php | 64 +- 21 files changed, 945 insertions(+), 1156 deletions(-) create mode 100644 src/Composer/DependencyResolver/LocalRepoTransaction.php create mode 100644 src/Composer/DependencyResolver/LockTransaction.php delete mode 100644 src/Composer/DependencyResolver/Transaction.php diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 051bc7449..7d241b482 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -44,7 +44,7 @@ class DefaultPolicy implements PolicyInterface return $constraint->matchSpecific($version, true); } - public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package, $mustMatchName = false) + public function findUpdatePackages(Pool $pool, PackageInterface $package, $mustMatchName = false) { $packages = array(); @@ -57,36 +57,34 @@ class DefaultPolicy implements PolicyInterface return $packages; } - public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null) + public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null) { - $packages = $this->groupLiteralsByNamePreferInstalled($pool, $installedMap, $literals); + $packages = $this->groupLiteralsByName($pool, $literals); - foreach ($packages as &$literals) { + foreach ($packages as &$nameLiterals) { $policy = $this; - usort($literals, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); + usort($nameLiterals, function ($a, $b) use ($policy, $pool, $requiredPackage) { + return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); }); } - foreach ($packages as &$literals) { - $literals = $this->pruneToHighestPriorityOrInstalled($pool, $installedMap, $literals); - - $literals = $this->pruneToBestVersion($pool, $literals); - - $literals = $this->pruneRemoteAliases($pool, $literals); + foreach ($packages as &$sortedLiterals) { + $sortedLiterals = $this->pruneToHighestPriority($pool, $sortedLiterals); + $sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals); + $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals); } $selected = call_user_func_array('array_merge', $packages); // now sort the result across all packages to respect replaces across packages - usort($selected, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); + usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) { + return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); }); return $selected; } - protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals) + protected function groupLiteralsByName(Pool $pool, $literals) { $packages = array(); foreach ($literals as $literal) { @@ -95,12 +93,7 @@ class DefaultPolicy implements PolicyInterface if (!isset($packages[$packageName])) { $packages[$packageName] = array(); } - - if (isset($installedMap[abs($literal)])) { - array_unshift($packages[$packageName], $literal); - } else { - $packages[$packageName][] = $literal; - } + $packages[$packageName][] = $literal; } return $packages; @@ -109,7 +102,7 @@ class DefaultPolicy implements PolicyInterface /** * @protected */ - public function compareByPriorityPreferInstalled(Pool $pool, array $installedMap, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false) + public function compareByPriority(Pool $pool, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false) { if ($a->getRepository() === $b->getRepository()) { // prefer aliases to the original package @@ -155,14 +148,6 @@ class DefaultPolicy implements PolicyInterface return ($a->id < $b->id) ? -1 : 1; } - if (isset($installedMap[$a->id])) { - return -1; - } - - if (isset($installedMap[$b->id])) { - return 1; - } - return ($pool->getPriority($a->id) > $pool->getPriority($b->id)) ? -1 : 1; } @@ -214,9 +199,9 @@ class DefaultPolicy implements PolicyInterface } /** - * Assumes that installed packages come first and then all highest priority packages + * Assumes that highest priority packages come first */ - protected function pruneToHighestPriorityOrInstalled(Pool $pool, array $installedMap, array $literals) + protected function pruneToHighestPriority(Pool $pool, array $literals) { $selected = array(); @@ -225,11 +210,6 @@ class DefaultPolicy implements PolicyInterface foreach ($literals as $literal) { $package = $pool->literalToPackage($literal); - if (isset($installedMap[$package->id])) { - $selected[] = $literal; - continue; - } - if (null === $priority) { $priority = $pool->getPriority($package->id); } diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php new file mode 100644 index 000000000..98c0b05d7 --- /dev/null +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -0,0 +1,185 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\Package\AliasPackage; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; + +/** + * @author Nils Adermann + */ +class LocalRepoTransaction +{ + /** @var RepositoryInterface */ + protected $lockedRepository; + + /** @var RepositoryInterface */ + protected $localRepository; + + public function __construct($lockedRepository, $localRepository) + { + $this->lockedRepository = $lockedRepository; + $this->localRepository = $localRepository; + + $this->operations = $this->calculateOperations(); + } + + public function getOperations() + { + return $this->operations; + } + + protected function calculateOperations() + { + $operations = array(); + + $localPackageMap = array(); + $removeMap = array(); + foreach ($this->localRepository->getPackages() as $package) { + if (isset($localPackageMap[$package->getName()])) { + die("Alias?"); + } + $localPackageMap[$package->getName()] = $package; + $removeMap[$package->getName()] = $package; + } + + $lockedPackages = array(); + foreach ($this->lockedRepository->getPackages() as $package) { + if (isset($localPackageMap[$package->getName()])) { + $source = $localPackageMap[$package->getName()]; + + // do we need to update? + if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } else { + // TODO do we need to update metadata? force update based on reference? + } + } else { + $operations[] = new Operation\InstallOperation($package); + unset($removeMap[$package->getName()]); + } + + +/* + if (isset($lockedPackages[$package->getName()])) { + die("Alias?"); + } + $lockedPackages[$package->getName()] = $package;*/ + } + + foreach ($removeMap as $name => $package) { + $operations[] = new Operation\UninstallOperation($package, null); + } + + $operations = $this->movePluginsToFront($operations); + $operations = $this->moveUninstallsToFront($operations); + + + // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? + /* + if ('update' === $jobType) { + $targetPackage = $operation->getTargetPackage(); + if ($targetPackage->isDev()) { + $initialPackage = $operation->getInitialPackage(); + if ($targetPackage->getVersion() === $initialPackage->getVersion() + && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) + && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) + ) { + $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); + $this->io->writeError('', true, IOInterface::DEBUG); + + continue; + } + } + }*/ + + return $operations; + } + + /** + * Workaround: if your packages depend on plugins, we must be sure + * that those are installed / updated first; else it would lead to packages + * being installed multiple times in different folders, when running Composer + * twice. + * + * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, + * it at least fixes the symptoms and makes usage of composer possible (again) + * in such scenarios. + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function movePluginsToFront(array $operations) + { + $pluginsNoDeps = array(); + $pluginsWithDeps = array(); + $pluginRequires = array(); + + foreach (array_reverse($operations, true) as $idx => $op) { + if ($op instanceof Operation\InstallOperation) { + $package = $op->getPackage(); + } elseif ($op instanceof Operation\UpdateOperation) { + $package = $op->getTargetPackage(); + } else { + continue; + } + + // is this package a plugin? + $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; + + // is this a plugin or a dependency of a plugin? + if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { + // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' + $requires = array_filter(array_keys($package->getRequires()), function ($req) { + return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); + + // is this a plugin with no meaningful dependencies? + if ($isPlugin && !count($requires)) { + // plugins with no dependencies go to the very front + array_unshift($pluginsNoDeps, $op); + } else { + // capture the requirements for this package so those packages will be moved up as well + $pluginRequires = array_merge($pluginRequires, $requires); + // move the operation to the front + array_unshift($pluginsWithDeps, $op); + } + + unset($operations[$idx]); + } + } + + return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); + } + + /** + * Removals of packages should be executed before installations in + * case two packages resolve to the same path (due to custom installers) + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function moveUninstallsToFront(array $operations) + { + $uninstOps = array(); + foreach ($operations as $idx => $op) { + if ($op instanceof UninstallOperation) { + $uninstOps[] = $op; + unset($operations[$idx]); + } + } + + return array_merge($uninstOps, $operations); + } +} diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php new file mode 100644 index 000000000..35cf760b9 --- /dev/null +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -0,0 +1,167 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\Package\AliasPackage; +use Composer\Package\RootAliasPackage; +use Composer\Package\RootPackageInterface; +use Composer\Repository\RepositoryInterface; + +/** + * @author Nils Adermann + */ +class LockTransaction +{ + protected $policy; + /** @var Pool */ + protected $pool; + + /** + * packages in current lock file, platform repo or otherwise present + * @var array + */ + protected $presentMap; + + /** + * Packages which cannot be mapped, platform repo, root package, other fixed repos + * @var array + */ + protected $unlockableMap; + + protected $decisions; + protected $transaction; + + public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions) + { + $this->policy = $policy; + $this->pool = $pool; + $this->presentMap = $presentMap; + $this->unlockableMap = $unlockableMap; + $this->decisions = $decisions; + + $this->operations = $this->calculateOperations(); + } + + /** + * @return OperationInterface[] + */ + public function getOperations() + { + return $this->operations; + } + + protected function calculateOperations() + { + $operations = array(); + $lockMeansUpdateMap = $this->findPotentialUpdates(); + + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + $reason = $decision[Decisions::DECISION_REASON]; + + $package = $this->pool->literalToPackage($literal); + + // wanted & !present + if ($literal > 0 && !isset($this->presentMap[$package->id])) { + if (isset($lockMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { + $operations[] = new Operation\UpdateOperation($lockMeansUpdateMap[abs($literal)], $package, $reason); + + // avoid updates to one package from multiple origins + unset($lockMeansUpdateMap[abs($literal)]); + $ignoreRemove[$source->id] = true; + } else { + if ($package instanceof AliasPackage) { + $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason); + } else { + $operations[] = new Operation\InstallOperation($package, $reason); + } + } + } + } + + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + $reason = $decision[Decisions::DECISION_REASON]; + $package = $this->pool->literalToPackage($literal); + + if ($literal <= 0 && isset($this->presentMap[$package->id]) && !isset($ignoreRemove[$package->id])) { + if ($package instanceof AliasPackage) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason); + } else { + $operations[] = new Operation\UninstallOperation($package, $reason); + } + } + } + + return $operations; + } + + // TODO additionalFixedRepository needs to be looked at here as well? + public function getNewLockNonDevPackages() + { + $packages = array(); + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + + if ($literal > 0) { + $package = $this->pool->literalToPackage($literal); + if (!isset($this->unlockableMap[$package->id]) && !($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + $packages[] = $package; + } + } + } + + return $packages; + } + + public function getNewLockDevPackages() + { + $packages = array(); + return $packages; + } + + protected function findPotentialUpdates() + { + $lockMeansUpdateMap = array(); + + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + $package = $this->pool->literalToPackage($literal); + + if ($package instanceof AliasPackage) { + continue; + } + + // !wanted & present + if ($literal <= 0 && isset($this->presentMap[$package->id])) { + // TODO can't we just look at existing rules? + $updates = $this->policy->findUpdatePackages($this->pool, $package); + + $literals = array($package->id); + + foreach ($updates as $update) { + $literals[] = $update->id; + } + + foreach ($literals as $updateLiteral) { + if ($updateLiteral !== $literal && !isset($lockMeansUpdateMap[$updateLiteral])) { + $lockMeansUpdateMap[$updateLiteral] = $package; + } + } + } + } + + return $lockMeansUpdateMap; + } +} diff --git a/src/Composer/DependencyResolver/PolicyInterface.php b/src/Composer/DependencyResolver/PolicyInterface.php index 3464bd594..d4db7f3a9 100644 --- a/src/Composer/DependencyResolver/PolicyInterface.php +++ b/src/Composer/DependencyResolver/PolicyInterface.php @@ -21,7 +21,7 @@ interface PolicyInterface { public function versionCompare(PackageInterface $a, PackageInterface $b, $operator); - public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package); + public function findUpdatePackages(Pool $pool, PackageInterface $package); - public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null); + public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null); } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 355ba25d4..e62d03999 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -12,18 +12,11 @@ namespace Composer\DependencyResolver; -use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; -use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; -use Composer\Repository\RepositoryInterface; -use Composer\Repository\CompositeRepository; -use Composer\Repository\ComposerRepository; -use Composer\Repository\InstalledRepositoryInterface; -use Composer\Repository\PlatformRepository; use Composer\Package\PackageInterface; /** diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 8ab6e5244..f1450d182 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -53,6 +53,10 @@ class PoolBuilder // TODO do we really want the request here? kind of want a root requirements thingy instead $loadNames = array(); + foreach ($request->getFixedPackages() as $package) { + // TODO can actually use very specific constraint + $loadNames[$package->getname()] = null; + } foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': @@ -99,7 +103,7 @@ class PoolBuilder } foreach ($this->packages as $i => $package) { - // we check all alias related packages at once, so no need ot check individual aliases + // 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 && isset($this->nameConstraints[$package->getName()])) { $constraint = $this->nameConstraints[$package->getName()]; diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 271c7261f..fed294fc6 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -220,7 +220,7 @@ class Problem if (isset($job['constraint'])) { $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); } else { - $packages = array(); + $packages = $this->pool->whatProvides($job['packageName'], null); } return 'Job(cmd='.$job['cmd'].', target='.$job['packageName'].', packages=['.$this->getPackageList($packages).'])'; diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 85dc9c4d0..a225e33b6 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -12,6 +12,10 @@ namespace Composer\DependencyResolver; +use Composer\Package\Package; +use Composer\Package\PackageInterface; +use Composer\Package\RootAliasPackage; +use Composer\Repository\RepositoryInterface; use Composer\Semver\Constraint\ConstraintInterface; /** @@ -19,11 +23,14 @@ use Composer\Semver\Constraint\ConstraintInterface; */ class Request { - protected $jobs; + protected $lockedRepository; + protected $jobs = array(); + protected $fixedPackages = array(); + protected $unlockables = array(); - public function __construct() + public function __construct(RepositoryInterface $lockedRepository = null) { - $this->jobs = array(); + $this->lockedRepository = $lockedRepository; } public function install($packageName, ConstraintInterface $constraint = null) @@ -31,11 +38,6 @@ class Request $this->addJob($packageName, 'install', $constraint); } - public function update($packageName, ConstraintInterface $constraint = null) - { - $this->addJob($packageName, 'update', $constraint); - } - public function remove($packageName, ConstraintInterface $constraint = null) { $this->addJob($packageName, 'remove', $constraint); @@ -43,18 +45,21 @@ class Request /** * Mark an existing package as being installed and having to remain installed - * - * These jobs will not be tempered with by the solver - * - * @param string $packageName - * @param ConstraintInterface|null $constraint */ - public function fix($packageName, ConstraintInterface $constraint = null) + public function fixPackage(PackageInterface $package, $lockable = true) { - $this->addJob($packageName, 'install', $constraint, true); + if ($package instanceof RootAliasPackage) { + $package = $package->getAliasOf(); + } + + $this->fixedPackages[] = $package; + + if (!$lockable) { + $this->unlockables[] = $package; + } } - protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null, $fixed = false) + protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null) { $packageName = strtolower($packageName); @@ -62,17 +67,48 @@ class Request 'cmd' => $cmd, 'packageName' => $packageName, 'constraint' => $constraint, - 'fixed' => $fixed, ); } - public function updateAll() - { - $this->jobs[] = array('cmd' => 'update-all'); - } - public function getJobs() { return $this->jobs; } + + public function getFixedPackages() + { + return $this->fixedPackages; + } + + public function getPresentMap() + { + $presentMap = array(); + + if ($this->lockedRepository) { + foreach ($this->lockedRepository as $package) { + $presentMap[$package->id] = $package; + } + } + + foreach ($this->fixedPackages as $package) { + $presentMap[$package->id] = $package; + } + + return $presentMap; + } + + public function getUnlockableMap() + { + $unlockableMap = array(); + + foreach ($this->unlockables as $package) { + $unlockableMap[$package->id] = $package; + } + + return $unlockableMap; + } + + public function getLockMap() + { + } } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 4533a6b61..c238f59fa 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -231,7 +231,7 @@ abstract class Rule case self::RULE_INSTALLED_PACKAGE_OBSOLETES: return $ruleText; case self::RULE_PACKAGE_SAME_NAME: - return 'Can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.'; + return 'Same name, can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.'; case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 9aaf564a5..0a63eaacb 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -24,8 +24,6 @@ class RuleSetGenerator protected $policy; protected $pool; protected $rules; - protected $jobs; - protected $installedMap; protected $addedMap; protected $conflictAddedMap; protected $addedPackages; @@ -218,8 +216,6 @@ class RuleSetGenerator } // check obsoletes and implicit obsoletes of a package - $isInstalled = isset($this->installedMap[$package->id]); - foreach ($package->getReplaces() as $link) { if (!isset($this->addedPackagesByNames[$link->getTarget()])) { continue; @@ -232,7 +228,7 @@ class RuleSetGenerator } if (!$this->obsoleteImpossibleForAlias($package, $provider)) { - $reason = $isInstalled ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES; + $reason = Rule::RULE_PACKAGE_OBSOLETES; $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $link)); } } @@ -254,21 +250,31 @@ class RuleSetGenerator return $impossible; } - protected function addRulesForJobs($ignorePlatformReqs) + protected function addRulesForRequest($request, $ignorePlatformReqs) { - foreach ($this->jobs as $job) { + foreach ($request->getFixedPackages() as $package) { + $this->addRulesForPackage($package, $ignorePlatformReqs); + + $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_JOB_INSTALL, array( + 'cmd' => 'fix', + 'packageName' => $package->getName(), + 'constraint' => null, + 'fixed' => true + )); + $this->addRule(RuleSet::TYPE_JOB, $rule); + } + + foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': - if (!$job['fixed'] && $ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { + if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { break; } $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); if ($packages) { foreach ($packages as $package) { - if (!isset($this->installedMap[$package->id])) { - $this->addRulesForPackage($package, $ignorePlatformReqs); - } + $this->addRulesForPackage($package, $ignorePlatformReqs); } $rule = $this->createInstallOneOfRule($packages, Rule::RULE_JOB_INSTALL, $job); @@ -288,21 +294,16 @@ class RuleSetGenerator } } - public function getRulesFor($jobs, $installedMap, $ignorePlatformReqs = false) + public function getRulesFor(Request $request, $ignorePlatformReqs = false) { - $this->jobs = $jobs; $this->rules = new RuleSet; - $this->installedMap = $installedMap; $this->addedMap = array(); $this->conflictAddedMap = array(); $this->addedPackages = array(); $this->addedPackagesByNames = array(); - foreach ($this->installedMap as $package) { - $this->addRulesForPackage($package, $ignorePlatformReqs); - } - $this->addRulesForJobs($ignorePlatformReqs); + $this->addRulesForRequest($request, $ignorePlatformReqs); $this->addConflictRules(); diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 2188d99ce..1a5905c67 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -29,23 +29,18 @@ class Solver protected $policy; /** @var Pool */ protected $pool = null; - /** @var RepositoryInterface */ - protected $installed; + /** @var RuleSet */ protected $rules; /** @var RuleSetGenerator */ protected $ruleSetGenerator; - /** @var array */ - protected $jobs; - /** @var int[] */ - protected $updateMap = array(); /** @var RuleWatchGraph */ protected $watchGraph; /** @var Decisions */ protected $decisions; - /** @var int[] */ - protected $installedMap; + /** @var Package[] */ + protected $fixedMap; /** @var int */ protected $propagateIndex; @@ -67,15 +62,13 @@ class Solver /** * @param PolicyInterface $policy * @param Pool $pool - * @param RepositoryInterface $installed * @param IOInterface $io */ - public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed, IOInterface $io) + public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io) { $this->io = $io; $this->policy = $policy; $this->pool = $pool; - $this->installed = $installed; } /** @@ -164,36 +157,22 @@ class Solver } } - protected function setupInstalledMap() + protected function setupFixedMap(Request $request) { - $this->installedMap = array(); - foreach ($this->installed->getPackages() as $package) { - $this->installedMap[$package->id] = $package; + $this->fixedMap = array(); + foreach ($request->getFixedPackages() as $package) { + $this->fixedMap[$package->id] = $package; } } /** + * @param Request $request * @param bool $ignorePlatformReqs */ - protected function checkForRootRequireProblems($ignorePlatformReqs) + protected function checkForRootRequireProblems($request, $ignorePlatformReqs) { - foreach ($this->jobs as $job) { + foreach ($request->getJobs() as $job) { switch ($job['cmd']) { - case 'update': - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); - foreach ($packages as $package) { - if (isset($this->installedMap[$package->id])) { - $this->updateMap[$package->id] = true; - } - } - break; - - case 'update-all': - foreach ($this->installedMap as $package) { - $this->updateMap[$package->id] = true; - } - break; - case 'install': if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { break; @@ -212,18 +191,16 @@ class Solver /** * @param Request $request * @param bool $ignorePlatformReqs - * @return array + * @return LockTransaction */ public function solve(Request $request, $ignorePlatformReqs = false) { - $this->jobs = $request->getJobs(); - - $this->setupInstalledMap(); + $this->setupFixedMap($request); $this->io->writeError('Generating rules', true, IOInterface::DEBUG); $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); - $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs); - $this->checkForRootRequireProblems($ignorePlatformReqs); + $this->rules = $this->ruleSetGenerator->getRulesFor($request, $ignorePlatformReqs); + $this->checkForRootRequireProblems($request, $ignorePlatformReqs); $this->decisions = new Decisions($this->pool); $this->watchGraph = new RuleWatchGraph; @@ -240,20 +217,11 @@ class Solver $this->io->writeError('', true, IOInterface::DEBUG); $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); - // decide to remove everything that's installed and undecided - foreach ($this->installedMap as $packageId => $void) { - if ($this->decisions->undecided($packageId)) { - $this->decisions->decide(-$packageId, 1, null); - } - } - if ($this->problems) { - throw new SolverProblemsException($this->problems, $this->installedMap, $this->learnedPool); + throw new SolverProblemsException($this->problems, $request->getPresentMap(), $this->learnedPool); } - $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions); - - return $transaction->getOperations(); + return new LockTransaction($this->policy, $this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); } /** @@ -392,7 +360,7 @@ class Solver private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule) { // choose best package to install from decisionQueue - $literals = $this->policy->selectPreferredPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage()); + $literals = $this->policy->selectPreferredPackages($this->pool, $decisionQueue, $rule->getRequiredPackage()); $selectedLiteral = array_shift($literals); @@ -728,19 +696,14 @@ class Solver } if ($noneSatisfied && count($decisionQueue)) { - // prune all update packages until installed version - // except for requested updates - if (count($this->installed) != count($this->updateMap)) { - $prunedQueue = array(); - foreach ($decisionQueue as $literal) { - if (isset($this->installedMap[abs($literal)])) { - $prunedQueue[] = $literal; - if (isset($this->updateMap[abs($literal)])) { - $prunedQueue = $decisionQueue; - break; - } - } + // if any of the options in the decision queue are fixed, only use those + $prunedQueue = array(); + foreach ($decisionQueue as $literal) { + if (isset($this->fixedMap[abs($literal)])) { + $prunedQueue[] = $literal; } + } + if (!empty($prunedQueue)) { $decisionQueue = $prunedQueue; } } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php deleted file mode 100644 index c8d3bbe53..000000000 --- a/src/Composer/DependencyResolver/Transaction.php +++ /dev/null @@ -1,244 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\DependencyResolver; - -use Composer\Package\AliasPackage; - -/** - * @author Nils Adermann - */ -class Transaction -{ - protected $policy; - protected $pool; - protected $installedMap; - protected $decisions; - protected $transaction; - - public function __construct($policy, $pool, $installedMap, $decisions) - { - $this->policy = $policy; - $this->pool = $pool; - $this->installedMap = $installedMap; - $this->decisions = $decisions; - $this->transaction = array(); - } - - public function getOperations() - { - $installMeansUpdateMap = $this->findUpdates(); - - $updateMap = array(); - $installMap = array(); - $uninstallMap = array(); - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - - $package = $this->pool->literalToPackage($literal); - - // wanted & installed || !wanted & !installed - if (($literal > 0) == isset($this->installedMap[$package->id])) { - continue; - } - - if ($literal > 0) { - if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { - $source = $installMeansUpdateMap[abs($literal)]; - - $updateMap[$package->id] = array( - 'package' => $package, - 'source' => $source, - 'reason' => $reason, - ); - - // avoid updates to one package from multiple origins - unset($installMeansUpdateMap[abs($literal)]); - $ignoreRemove[$source->id] = true; - } else { - $installMap[$package->id] = array( - 'package' => $package, - 'reason' => $reason, - ); - } - } - } - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - $package = $this->pool->literalToPackage($literal); - - if ($literal <= 0 && - isset($this->installedMap[$package->id]) && - !isset($ignoreRemove[$package->id])) { - $uninstallMap[$package->id] = array( - 'package' => $package, - 'reason' => $reason, - ); - } - } - - $this->transactionFromMaps($installMap, $updateMap, $uninstallMap); - - return $this->transaction; - } - - protected function transactionFromMaps($installMap, $updateMap, $uninstallMap) - { - $queue = array_map( - function ($operation) { - return $operation['package']; - }, - $this->findRootPackages($installMap, $updateMap) - ); - - $visited = array(); - - while (!empty($queue)) { - $package = array_pop($queue); - $packageId = $package->id; - - if (!isset($visited[$packageId])) { - $queue[] = $package; - - if ($package instanceof AliasPackage) { - $queue[] = $package->getAliasOf(); - } else { - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); - - foreach ($possibleRequires as $require) { - $queue[] = $require; - } - } - } - - $visited[$package->id] = true; - } else { - if (isset($installMap[$packageId])) { - $this->install( - $installMap[$packageId]['package'], - $installMap[$packageId]['reason'] - ); - unset($installMap[$packageId]); - } - if (isset($updateMap[$packageId])) { - $this->update( - $updateMap[$packageId]['source'], - $updateMap[$packageId]['package'], - $updateMap[$packageId]['reason'] - ); - unset($updateMap[$packageId]); - } - } - } - - foreach ($uninstallMap as $uninstall) { - $this->uninstall($uninstall['package'], $uninstall['reason']); - } - } - - protected function findRootPackages($installMap, $updateMap) - { - $packages = $installMap + $updateMap; - $roots = $packages; - - foreach ($packages as $packageId => $operation) { - $package = $operation['package']; - - if (!isset($roots[$packageId])) { - continue; - } - - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); - - foreach ($possibleRequires as $require) { - if ($require !== $package) { - unset($roots[$require->id]); - } - } - } - } - - return $roots; - } - - protected function findUpdates() - { - $installMeansUpdateMap = array(); - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $package = $this->pool->literalToPackage($literal); - - if ($package instanceof AliasPackage) { - continue; - } - - // !wanted & installed - if ($literal <= 0 && isset($this->installedMap[$package->id])) { - $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package); - - $literals = array($package->id); - - foreach ($updates as $update) { - $literals[] = $update->id; - } - - foreach ($literals as $updateLiteral) { - if ($updateLiteral !== $literal) { - $installMeansUpdateMap[abs($updateLiteral)] = $package; - } - } - } - } - - return $installMeansUpdateMap; - } - - protected function install($package, $reason) - { - if ($package instanceof AliasPackage) { - return $this->markAliasInstalled($package, $reason); - } - - $this->transaction[] = new Operation\InstallOperation($package, $reason); - } - - protected function update($from, $to, $reason) - { - $this->transaction[] = new Operation\UpdateOperation($from, $to, $reason); - } - - protected function uninstall($package, $reason) - { - if ($package instanceof AliasPackage) { - return $this->markAliasUninstalled($package, $reason); - } - - $this->transaction[] = new Operation\UninstallOperation($package, $reason); - } - - protected function markAliasInstalled($package, $reason) - { - $this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason); - } - - protected function markAliasUninstalled($package, $reason) - { - $this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason); - } -} diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index c37d7cf45..c45374e5d 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -19,6 +19,7 @@ use Composer\IO\IOInterface; use Composer\Composer; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositorySet; use Composer\Script; use Composer\Installer\PackageEvent; @@ -130,9 +131,9 @@ class EventDispatcher * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations = array()) + public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $lockedRepo, Request $request, array $operations = array()) { - return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $installedRepo, $request, $operations)); + return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $lockedRepo, $request, $operations)); } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 10f115daf..07433c911 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -14,6 +14,7 @@ namespace Composer; use Composer\Autoload\AutoloadGenerator; use Composer\DependencyResolver\DefaultPolicy; +use Composer\DependencyResolver\LocalRepoTransaction; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -74,6 +75,12 @@ class Installer */ protected $package; + // TODO can we get rid of the below and just use the package itself? + /** + * @var RootPackageInterface + */ + protected $fixedRootPackage; + /** * @var DownloadManager */ @@ -139,7 +146,7 @@ class Installer /** * @var RepositoryInterface */ - protected $additionalInstalledRepository; + protected $additionalFixedRepository; /** * Constructor @@ -184,6 +191,7 @@ class Installer // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { + // TODO throw an error instead? $this->update = true; } @@ -202,6 +210,7 @@ class Installer putenv("COMPOSER_DEV_MODE=$devMode"); // dispatch pre event + // should we treat this more strictly as running an update and then running an install, triggering events multiple times? $eventName = $this->update ? ScriptEvents::PRE_UPDATE_CMD : ScriptEvents::PRE_INSTALL_CMD; $this->eventDispatcher->dispatchScript($eventName, $this->devMode); } @@ -209,15 +218,8 @@ class Installer $this->downloadManager->setPreferSource($this->preferSource); $this->downloadManager->setPreferDist($this->preferDist); - // create installed repo, this contains all local packages + platform packages (php & extensions) $localRepo = $this->repositoryManager->getLocalRepository(); - if ($this->update) { - $platformOverrides = $this->config->get('platform') ?: array(); - } else { - $platformOverrides = $this->locker->getPlatformOverrides(); - } - $platformRepo = new PlatformRepository(array(), $platformOverrides); - $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); + $platformRepo = $this->createPlatformRepo($this->update); $aliases = $this->getRootAliases(); $this->aliasPlatformPackages($platformRepo, $aliases); @@ -227,7 +229,12 @@ class Installer } try { - list($res, $devPackages) = $this->doInstall($localRepo, $installedRepo, $platformRepo, $aliases); + // TODO what are installs? does locking a package without downloading code count? + if ($this->update) { + $res = $this->doUpdate($localRepo, $platformRepo, $aliases, true); + } else { + $res = $this->doInstall($localRepo, $platformRepo, $aliases, false); + } if ($res !== 0) { return $res; } @@ -243,10 +250,11 @@ class Installer } // output suggestions if we're in dev mode - if ($this->devMode && !$this->skipSuggest) { - $this->suggestedPackagesReporter->output($installedRepo); + if ($this->update && $this->devMode && !$this->skipSuggest) { + $this->suggestedPackagesReporter->output($this->locker->getLockedRepository($this->devMode)); } + // TODO probably makes more sense to do this on the lock file only? # Find abandoned packages and warn user foreach ($localRepo->getPackages() as $package) { if (!$package instanceof CompletePackage || !$package->isAbandoned()) { @@ -266,30 +274,6 @@ class Installer ); } - // write lock - if ($this->update && $this->writeLock) { - $localRepo->reload(); - - $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); - $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); - - $updatedLock = $this->locker->setLockData( - array_diff($localRepo->getCanonicalPackages(), $devPackages), - $devPackages, - $platformReqs, - $platformDevReqs, - $aliases, - $this->package->getMinimumStability(), - $this->package->getStabilityFlags(), - $this->preferStable || $this->package->getPreferStable(), - $this->preferLowest, - $this->config->get('platform') ?: array() - ); - if ($updatedLock) { - $this->io->writeError('Writing lock file'); - } - } - if ($this->dumpAutoloader) { // write autoloader if ($this->optimizeAutoloader) { @@ -333,181 +317,284 @@ class Installer return 0; } + protected function doUpdate(RepositoryInterface $localRepo, PlatformRepository $platformRepo, $aliases, $doInstall) + { + $lockedRepository = null; + + if ($this->locker->isLocked()) { + $lockedRepository = $this->locker->getLockedRepository(true); + } + + if ($this->updateWhitelist) { + if (!$lockedRepository) { + $this->io->writeError('Cannot update only a partial set of packages without a lock file present.', true, IOInterface::QUIET); + return 1; + } + $this->whitelistUpdateDependencies( + $lockedRepository, + $this->package->getRequires(), + $this->package->getDevRequires() + ); + } + + $this->io->writeError('Loading composer repositories with package information'); + + // creating repository set + $policy = $this->createPolicy(true); + $repositorySet = $this->createRepositorySet($platformRepo, $aliases); + $repositories = $this->repositoryManager->getRepositories(); + foreach ($repositories as $repository) { + $repositorySet->addRepository($repository); + } + if ($lockedRepository) { + $repositorySet->addRepository($lockedRepository); + } + // TODO can we drop any locked packages that we have matching remote versions for? + + $request = $this->createRequest($this->fixedRootPackage, $platformRepo); + + if ($lockedRepository) { + // TODO do we really always need this? Maybe only to skip fix() in updateWhitelist case cause these packages get removed on full update automatically? + foreach ($lockedRepository->getPackages() as $lockedPackage) { + if (!$repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { + $constraint = new Constraint('=', $lockedPackage->getVersion()); + $constraint->setPrettyString('(stability not acceptable)'); + $request->remove($lockedPackage->getName(), $constraint); + } + } + } + + $this->io->writeError('Updating dependencies'); + + $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); + + foreach ($links as $link) { + $request->install($link->getTarget(), $link->getConstraint()); + } + + // if the updateWhitelist is enabled, packages not in it are also fixed + // to the version specified in the lock + if ($this->updateWhitelist) { + foreach ($lockedRepository->getPackages() as $lockedPackage) { + if (!$this->isUpdateable($lockedPackage) && $repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { + // TODO add reason for fix? + $request->fixPackage($lockedPackage); + } + } + } + + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); + + $pool = $repositorySet->createPool($request); + + // TODO ensure that the solver always picks most recent reference for dev packages, so they get updated even when just a new commit is pushed but version is unchanged + + // solve dependencies + $solver = new Solver($policy, $pool, $this->io); + try { + $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + } catch (SolverProblemsException $e) { + $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError($e->getMessage()); + if (!$this->devMode) { + $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); + } + + return max(1, $e->getCode()); + } + + // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $lockedRepository, $request, $lockTransaction); + + $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); + $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE); + + if (!$lockTransaction->getOperations()) { + $this->io->writeError('Nothing to modify in lock file'); + } + + // write lock + $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); + $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); + + $updatedLock = $this->locker->setLockData( + $lockTransaction->getNewLockNonDevPackages(), + $lockTransaction->getNewLockDevPackages(), + $platformReqs, + $platformDevReqs, + $aliases, + $this->package->getMinimumStability(), + $this->package->getStabilityFlags(), + $this->preferStable || $this->package->getPreferStable(), + $this->preferLowest, + $this->config->get('platform') ?: array(), + $this->writeLock && $this->executeOperations + ); + if ($updatedLock && $this->writeLock && $this->executeOperations) { + $this->io->writeError('Writing lock file'); + } + + if ($lockTransaction->getOperations()) { + $installs = $updates = $uninstalls = array(); + foreach ($lockTransaction->getOperations() as $operation) { + if ($operation instanceof InstallOperation) { + $installs[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); + } elseif ($operation instanceof UpdateOperation) { + $updates[] = $operation->getTargetPackage()->getPrettyName().':'.$operation->getTargetPackage()->getFullPrettyVersion(); + } elseif ($operation instanceof UninstallOperation) { + $uninstalls[] = $operation->getPackage()->getPrettyName(); + } + } + + $this->io->writeError(sprintf( + "Lock file operations: %d install%s, %d update%s, %d removal%s", + count($installs), + 1 === count($installs) ? '' : 's', + count($updates), + 1 === count($updates) ? '' : 's', + count($uninstalls), + 1 === count($uninstalls) ? '' : 's' + )); + if ($installs) { + $this->io->writeError("Installs: ".implode(', ', $installs), true, IOInterface::VERBOSE); + } + if ($updates) { + $this->io->writeError("Updates: ".implode(', ', $updates), true, IOInterface::VERBOSE); + } + if ($uninstalls) { + $this->io->writeError("Removals: ".implode(', ', $uninstalls), true, IOInterface::VERBOSE); + } + } + + foreach ($lockTransaction->getOperations() as $operation) { + // collect suggestions + $jobType = $operation->getJobType(); + if ($operation instanceof InstallOperation) { + $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); + } + + // TODO should this really happen here or should this be written to the lock file? + // updating, force dev packages' references if they're in root package refs + $package = null; + if ('update' === $jobType) { + $package = $operation->getTargetPackage(); + } elseif ('install' === $jobType) { + $package = $operation->getPackage(); + } + + if ($package && $package->isDev()) { + $references = $this->package->getReferences(); + if (isset($references[$package->getName()])) { + $this->updateInstallReferences($package, $references[$package->getName()]); + } + } + + // output op, but alias op only in debug verbosity + if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { + $this->io->writeError(' - ' . $operation); + } + + // output reasons why the operation was run, only for install/update operations + if ($this->verbose && $this->io->isVeryVerbose() && in_array($jobType, array('install', 'update'))) { + $reason = $operation->getReason(); + if ($reason instanceof Rule) { + switch ($reason->getReason()) { + case Rule::RULE_JOB_INSTALL: + $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); + $this->io->writeError(''); + break; + case Rule::RULE_PACKAGE_REQUIRES: + $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); + $this->io->writeError(''); + break; + } + } + } + } + + if ($doInstall) { + // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false + return $this->doInstall($localRepo, $platformRepo, $aliases, true); + } + + return 0; + } + /** * @param RepositoryInterface $localRepo * @param RepositoryInterface $installedRepo * @param PlatformRepository $platformRepo * @param array $aliases - * @return array [int, PackageInterfaces[]|null] with the exit code and an array of dev packages on update, or null on install + * @return int exit code */ - protected function doInstall($localRepo, $installedRepo, $platformRepo, $aliases) + protected function doInstall(RepositoryInterface $localRepo, PlatformRepository $platformRepo, $aliases, $alreadySolved = false) { - // init vars - $lockedRepository = null; - $repositories = null; - - // initialize locked repo if we are installing from lock or in a partial update - // and a lock file is present as we need to force install non-whitelisted lock file - // packages in that case - if (!$this->update || (!empty($this->updateWhitelist) && $this->locker->isLocked())) { - $lockedRepository = $this->locker->getLockedRepository($this->devMode); - } - - $this->whitelistUpdateDependencies( - $lockedRepository ?: $localRepo, - $this->package->getRequires(), - $this->package->getDevRequires() - ); - - $this->io->writeError('Loading composer repositories with package information'); + $lockedRepository = $this->locker->getLockedRepository($this->devMode); // creating repository set - $policy = $this->createPolicy(); - $repositorySet = $this->createRepositorySet($aliases, $this->update ? null : $lockedRepository); - $repositorySet->addRepository($installedRepo); - if ($this->update) { - $repositories = $this->repositoryManager->getRepositories(); - foreach ($repositories as $repository) { - $repositorySet->addRepository($repository); - } - } - // Add the locked repository after the others in case we are doing a - // partial update so missing packages can be found there still. - // For installs from lock it's the only one added so it is first - if ($lockedRepository) { - $repositorySet->addRepository($lockedRepository); - } + $policy = $this->createPolicy(false); + $repositorySet = $this->createRepositorySet($platformRepo, $aliases, $lockedRepository); + $repositorySet->addRepository($lockedRepository); - // creating requirements request - $request = $this->createRequest($this->package, $platformRepo); + $this->io->writeError('Installing dependencies'.($this->devMode ? ' (including require-dev)' : '').' from lock file'); - if ($this->update) { - // remove unstable packages from the localRepo if they don't match the current stability settings - $removedUnstablePackages = array(); - foreach ($localRepo->getPackages() as $package) { - if ( - !$repositorySet->isPackageAcceptable($package->getNames(), $package->getStability()) - && $this->installationManager->isPackageInstalled($localRepo, $package) - ) { - $removedUnstablePackages[$package->getName()] = true; - $request->remove($package->getName(), new Constraint('=', $package->getVersion())); - } - } + // verify that the lock file works with the current platform repository + // we can skip this part if we're doing this as the second step after an update + if (!$alreadySolved) { + $this->io->writeError('Verifying lock file contents can be installed on current platform.'); - $this->io->writeError('Updating dependencies'.($this->devMode ? ' (including require-dev)' : '').''); - - $request->updateAll(); - - $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); - - foreach ($links as $link) { - $request->install($link->getTarget(), $link->getConstraint()); - } - - // if the updateWhitelist is enabled, packages not in it are also fixed - // to the version specified in the lock, or their currently installed version - if ($this->updateWhitelist) { - $currentPackages = $this->getCurrentPackages($installedRepo); - - // collect packages to fixate from root requirements as well as installed packages - $candidates = array(); - foreach ($links as $link) { - $candidates[$link->getTarget()] = true; - $rootRequires[$link->getTarget()] = $link; - } - foreach ($currentPackages as $package) { - $candidates[$package->getName()] = true; - } - - // fix them to the version in lock (or currently installed) if they are not updateable - foreach ($candidates as $candidate => $dummy) { - foreach ($currentPackages as $curPackage) { - if ($curPackage->getName() === $candidate) { - if (!$this->isUpdateable($curPackage) && !isset($removedUnstablePackages[$curPackage->getName()])) { - $constraint = new Constraint('=', $curPackage->getVersion()); - $description = $this->locker->isLocked() ? '(locked at' : '(installed at'; - $requiredAt = isset($rootRequires[$candidate]) ? ', required as ' . $rootRequires[$candidate]->getPrettyConstraint() : ''; - $constraint->setPrettyString($description . ' ' . $curPackage->getPrettyVersion() . $requiredAt . ')'); - $request->install($curPackage->getName(), $constraint); - } - break; - } - } - } - } - } else { - $this->io->writeError('Installing dependencies'.($this->devMode ? ' (including require-dev)' : '').' from lock file'); + // creating requirements request + $request = $this->createRequest($this->fixedRootPackage, $platformRepo); if (!$this->locker->isFresh()) { $this->io->writeError('Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. Run update to update them.', true, IOInterface::QUIET); } foreach ($lockedRepository->getPackages() as $package) { - $version = $package->getVersion(); - if (isset($aliases[$package->getName()][$version])) { - $version = $aliases[$package->getName()][$version]['alias_normalized']; - } - $constraint = new Constraint('=', $version); - $constraint->setPrettyString($package->getPrettyVersion()); - $request->install($package->getName(), $constraint); + $request->fixPackage($package); } foreach ($this->locker->getPlatformRequirements($this->devMode) as $link) { $request->install($link->getTarget(), $link->getConstraint()); } - } - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); - $pool = $repositorySet->createPool($request); + $pool = $repositorySet->createPool($request); - // force dev packages to have the latest links if we update or install from a (potentially new) lock - $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); + // solve dependencies + $solver = new Solver($policy, $pool, $this->io); + try { + $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); - // solve dependencies - $solver = new Solver($policy, $pool, $installedRepo, $this->io); - try { - $operations = $solver->solve($request, $this->ignorePlatformReqs); - } catch (SolverProblemsException $e) { - $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); - $this->io->writeError($e->getMessage()); - if ($this->update && !$this->devMode) { - $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); + // installing the locked packages on this platfom resulted in lock modifying operations, there wasn't a conflict, but the lock file as-is seems to not work on this system + if (0 !== count($lockTransaction->getOperations())) { + $this->io->writeError('Your lock file cannot be installed on this system without changes, please run composer update.', true, IOInterface::QUIET); + // TODO actually display operations to explain what happened? + return 1; + } + } catch (SolverProblemsException $e) { + $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError($e->getMessage()); + + return max(1, $e->getCode()); } - return array(max(1, $e->getCode()), array()); + // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); } - // force dev packages to be updated if we update or install from a (potentially new) lock - $operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-updates', $operations); + // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations); + $localRepoTransaction = new LocalRepoTransaction($lockedRepository, $localRepo); - $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); - $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE); - - // execute operations - if (!$operations) { - $this->io->writeError('Nothing to install or update'); + if (!$localRepoTransaction->getOperations()) { + $this->io->writeError('Nothing to install, update or remove'); } - $operations = $this->movePluginsToFront($operations); - $operations = $this->moveUninstallsToFront($operations); - - // extract dev packages and mark them to be skipped if it's a --no-dev install or update - // we also force them to be uninstalled if they are present in the local repo - if ($this->update) { - $devPackages = $this->extractDevPackages($operations, $localRepo, $platformRepo, $aliases); - if (!$this->devMode) { - $operations = $this->filterDevPackageOperations($devPackages, $operations, $localRepo); - } - } else { - $devPackages = null; - } - - if ($operations) { + if ($localRepoTransaction->getOperations()) { $installs = $updates = $uninstalls = array(); - foreach ($operations as $operation) { + foreach ($localRepoTransaction->getOperations() as $operation) { if ($operation instanceof InstallOperation) { $installs[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UpdateOperation) { @@ -537,307 +624,42 @@ class Installer } } - foreach ($operations as $operation) { - // collect suggestions + foreach ($localRepoTransaction->getOperations() as $operation) { $jobType = $operation->getJobType(); - if ('install' === $jobType) { - $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); - } - - // updating, force dev packages' references if they're in root package refs - if ($this->update) { - $package = null; - if ('update' === $jobType) { - $package = $operation->getTargetPackage(); - } elseif ('install' === $jobType) { - $package = $operation->getPackage(); - } - if ($package && $package->isDev()) { - $references = $this->package->getReferences(); - if (isset($references[$package->getName()])) { - $this->updateInstallReferences($package, $references[$package->getName()]); - } - } - if ('update' === $jobType) { - $targetPackage = $operation->getTargetPackage(); - if ($targetPackage->isDev()) { - $initialPackage = $operation->getInitialPackage(); - if ($targetPackage->getVersion() === $initialPackage->getVersion() - && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) - && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) - ) { - $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); - $this->io->writeError('', true, IOInterface::DEBUG); - - continue; - } - } - } - } - $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($jobType); if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); + //$this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); } - // output non-alias ops when not executing operations (i.e. dry run), output alias ops in debug verbosity - if (!$this->executeOperations && false === strpos($operation->getJobType(), 'Alias')) { - $this->io->writeError(' - ' . $operation); - } elseif ($this->io->isDebug() && false !== strpos($operation->getJobType(), 'Alias')) { + // output op, but alias op only in debug verbosity + if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation); } $this->installationManager->execute($localRepo, $operation); - // output reasons why the operation was ran, only for install/update operations - if ($this->verbose && $this->io->isVeryVerbose() && in_array($jobType, array('install', 'update'))) { - $reason = $operation->getReason(); - if ($reason instanceof Rule) { - switch ($reason->getReason()) { - case Rule::RULE_JOB_INSTALL: - $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - case Rule::RULE_PACKAGE_REQUIRES: - $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - } - } - } - - if ($this->executeOperations || $this->writeLock) { + if ($this->executeOperations) { $localRepo->write(); } $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); + //$this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); } } - if ($this->executeOperations) { - // force source/dist urls to be updated for all packages - $this->processPackageUrls($pool, $policy, $localRepo, $repositories); - $localRepo->write(); - } - - return array(0, $devPackages); + return 0; } - /** - * Extracts the dev packages out of the localRepo - * - * This works by faking the operations so we can see what the dev packages - * would be at the end of the operation execution. This lets us then remove - * the dev packages from the list of operations accordingly if we are in a - * --no-dev install or update. - * - * @return array - */ - private function extractDevPackages(array $operations, RepositoryInterface $localRepo, PlatformRepository $platformRepo, array $aliases) + private function createPlatformRepo($forUpdate) { - if (!$this->package->getDevRequires()) { - return array(); + if ($forUpdate) { + $platformOverrides = $this->config->get('platform') ?: array(); + } else { + $platformOverrides = $this->locker->getPlatformOverrides(); } - // fake-apply all operations to this clone of the local repo so we see the complete set of package we would end up with - $tempLocalRepo = clone $localRepo; - foreach ($operations as $operation) { - switch ($operation->getJobType()) { - case 'install': - case 'markAliasInstalled': - if (!$tempLocalRepo->hasPackage($operation->getPackage())) { - $tempLocalRepo->addPackage(clone $operation->getPackage()); - } - break; - - case 'uninstall': - case 'markAliasUninstalled': - $tempLocalRepo->removePackage($operation->getPackage()); - break; - - case 'update': - $tempLocalRepo->removePackage($operation->getInitialPackage()); - if (!$tempLocalRepo->hasPackage($operation->getTargetPackage())) { - $tempLocalRepo->addPackage(clone $operation->getTargetPackage()); - } - break; - - default: - throw new \LogicException('Unknown type: '.$operation->getJobType()); - } - } - - // we have to reload the local repo to handle aliases properly - // but as it is not persisted on disk we use a loader/dumper - // to reload it in memory - $localRepo = new InstalledArrayRepository(array()); - $loader = new ArrayLoader(null, true); - $dumper = new ArrayDumper(); - foreach ($tempLocalRepo->getCanonicalPackages() as $pkg) { - $localRepo->addPackage($loader->load($dumper->dump($pkg))); - } - unset($tempLocalRepo, $loader, $dumper); - - $policy = $this->createPolicy(); - $repositorySet = $this->createRepositorySet($aliases); - $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); - $repositorySet->addRepository($installedRepo); - - // creating requirements request without dev requirements - $request = $this->createRequest($this->package, $platformRepo); - $request->updateAll(); - foreach ($this->package->getRequires() as $link) { - $request->install($link->getTarget(), $link->getConstraint()); - } - - // solve deps to see which get removed - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request); - $solver = new Solver($policy, $repositorySet->createPool($request), $installedRepo, $this->io); - $ops = $solver->solve($request, $this->ignorePlatformReqs); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request, $ops); - - $devPackages = array(); - foreach ($ops as $op) { - if ($op->getJobType() === 'uninstall') { - $devPackages[] = $op->getPackage(); - } - } - - return $devPackages; - } - - /** - * @return OperationInterface[] filtered operations, dev packages are uninstalled and all operations on them ignored - */ - private function filterDevPackageOperations(array $devPackages, array $operations, RepositoryInterface $localRepo) - { - $finalOps = array(); - $packagesToSkip = array(); - foreach ($devPackages as $pkg) { - $packagesToSkip[$pkg->getName()] = true; - if ($installedDevPkg = $localRepo->findPackage($pkg->getName(), '*')) { - if ($installedDevPkg instanceof AliasPackage) { - $finalOps[] = new MarkAliasUninstalledOperation($installedDevPkg, 'non-dev install removing it'); - $installedDevPkg = $installedDevPkg->getAliasOf(); - } - $finalOps[] = new UninstallOperation($installedDevPkg, 'non-dev install removing it'); - } - } - - // skip operations applied on dev packages - foreach ($operations as $op) { - $package = $op->getJobType() === 'update' ? $op->getTargetPackage() : $op->getPackage(); - if (isset($packagesToSkip[$package->getName()])) { - continue; - } - - $finalOps[] = $op; - } - - return $finalOps; - } - - /** - * Workaround: if your packages depend on plugins, we must be sure - * that those are installed / updated first; else it would lead to packages - * being installed multiple times in different folders, when running Composer - * twice. - * - * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, - * it at least fixes the symptoms and makes usage of composer possible (again) - * in such scenarios. - * - * @param OperationInterface[] $operations - * @return OperationInterface[] reordered operation list - */ - private function movePluginsToFront(array $operations) - { - $pluginsNoDeps = array(); - $pluginsWithDeps = array(); - $pluginRequires = array(); - - foreach (array_reverse($operations, true) as $idx => $op) { - if ($op instanceof InstallOperation) { - $package = $op->getPackage(); - } elseif ($op instanceof UpdateOperation) { - $package = $op->getTargetPackage(); - } else { - continue; - } - - // is this package a plugin? - $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; - - // is this a plugin or a dependency of a plugin? - if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { - // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' - $requires = array_filter(array_keys($package->getRequires()), function ($req) { - return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); - }); - - // is this a plugin with no meaningful dependencies? - if ($isPlugin && !count($requires)) { - // plugins with no dependencies go to the very front - array_unshift($pluginsNoDeps, $op); - } else { - // capture the requirements for this package so those packages will be moved up as well - $pluginRequires = array_merge($pluginRequires, $requires); - // move the operation to the front - array_unshift($pluginsWithDeps, $op); - } - - unset($operations[$idx]); - } - } - - return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); - } - - /** - * Removals of packages should be executed before installations in - * case two packages resolve to the same path (due to custom installers) - * - * @param OperationInterface[] $operations - * @return OperationInterface[] reordered operation list - */ - private function moveUninstallsToFront(array $operations) - { - $uninstOps = array(); - foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation) { - $uninstOps[] = $op; - unset($operations[$idx]); - } - } - - return array_merge($uninstOps, $operations); - } - - /** - * @return RepositoryInterface - */ - private function createInstalledRepo(RepositoryInterface $localRepo, PlatformRepository $platformRepo) - { - // clone root package to have one in the installed repo that does not require anything - // we don't want it to be uninstallable, but its requirements should not conflict - // with the lock file for example - $installedRootPackage = clone $this->package; - $installedRootPackage->setRequires(array()); - $installedRootPackage->setDevRequires(array()); - - $repos = array( - $localRepo, - new InstalledArrayRepository(array($installedRootPackage)), - $platformRepo, - ); - $installedRepo = new CompositeRepository($repos); - if ($this->additionalInstalledRepository) { - $installedRepo->addRepository($this->additionalInstalledRepository); - } - - return $installedRepo; + return new PlatformRepository(array(), $platformOverrides); } /** @@ -845,8 +667,10 @@ class Installer * @param RepositoryInterface|null $lockedRepository * @return RepositorySet */ - private function createRepositorySet(array $rootAliases = array(), $lockedRepository = null) + private function createRepositorySet(PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) { + // TODO what's the point of rootConstraints at all, we generate the package pool taking them into account anyway? + // TODO maybe we can drop the lockedRepository here if ($this->update) { $minimumStability = $this->package->getMinimumStability(); $stabilityFlags = $this->package->getStabilityFlags(); @@ -877,17 +701,28 @@ class Installer } } - return new RepositorySet($rootAliases, $minimumStability, $stabilityFlags, $rootConstraints); + $this->fixedRootPackage = clone $this->package; + $this->fixedRootPackage->setRequires(array()); + $this->fixedRootPackage->setDevRequires(array()); + + $repositorySet = new RepositorySet($rootAliases, $minimumStability, $stabilityFlags, $rootConstraints); + $repositorySet->addRepository(new InstalledArrayRepository(array($this->fixedRootPackage))); + $repositorySet->addRepository($platformRepo); + if ($this->additionalFixedRepository) { + $repositorySet->addRepository($this->additionalFixedRepository); + } + + return $repositorySet; } /** * @return DefaultPolicy */ - private function createPolicy() + private function createPolicy($forUpdate) { $preferStable = null; $preferLowest = null; - if (!$this->update) { + if (!$forUpdate) { $preferStable = $this->locker->getPreferStable(); $preferLowest = $this->locker->getPreferLowest(); } @@ -912,191 +747,30 @@ class Installer { $request = new Request(); - $constraint = new Constraint('=', $rootPackage->getVersion()); - $constraint->setPrettyString($rootPackage->getPrettyVersion()); - $request->install($rootPackage->getName(), $constraint); + $request->fixPackage($rootPackage, false); $fixedPackages = $platformRepo->getPackages(); - if ($this->additionalInstalledRepository) { - $additionalFixedPackages = $this->additionalInstalledRepository->getPackages(); - $fixedPackages = array_merge($fixedPackages, $additionalFixedPackages); + if ($this->additionalFixedRepository) { + $fixedPackages = array_merge($fixedPackages, $this->additionalFixedRepository->getPackages()); } // fix the version of all platform packages + additionally installed packages // to prevent the solver trying to remove or update those + // TODO why not replaces? $provided = $rootPackage->getProvides(); foreach ($fixedPackages as $package) { - $constraint = new Constraint('=', $package->getVersion()); - $constraint->setPrettyString($package->getPrettyVersion()); - // skip platform packages that are provided by the root package if ($package->getRepository() !== $platformRepo || !isset($provided[$package->getName()]) - || !$provided[$package->getName()]->getConstraint()->matches($constraint) + || !$provided[$package->getName()]->getConstraint()->matches(new Constraint('=', $package->getVersion())) ) { - $request->fix($package->getName(), $constraint); + $request->fixPackage($package, false); } } return $request; } - /** - * @param WritableRepositoryInterface $localRepo - * @param Pool $pool - * @param PolicyInterface $policy - * @param array $repositories - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $lockedRepository - * @param string $task - * @param array|null $operations - * @return array - */ - private function processDevPackages($localRepo, Pool $pool, $policy, $repositories, $installedRepo, $lockedRepository, $task, array $operations = null) - { - if ($task === 'force-updates' && null === $operations) { - throw new \InvalidArgumentException('Missing operations argument'); - } - if ($task === 'force-links') { - $operations = array(); - } - - if ($this->update && $this->updateWhitelist) { - $currentPackages = $this->getCurrentPackages($installedRepo); - } - - foreach ($localRepo->getCanonicalPackages() as $package) { - // skip non-dev packages - if (!$package->isDev()) { - continue; - } - - // skip packages that will be updated/uninstalled - foreach ($operations as $operation) { - if (('update' === $operation->getJobType() && $operation->getInitialPackage()->equals($package)) - || ('uninstall' === $operation->getJobType() && $operation->getPackage()->equals($package)) - ) { - continue 2; - } - } - - if ($this->update) { - // skip package if the whitelist is enabled and it is not in it - if ($this->updateWhitelist && !$this->isUpdateable($package)) { - // check if non-updateable packages are out of date compared to the lock file to ensure we don't corrupt it - foreach ($currentPackages as $curPackage) { - if ($curPackage->isDev() && $curPackage->getName() === $package->getName() && $curPackage->getVersion() === $package->getVersion()) { - if ($task === 'force-links') { - $package->setRequires($curPackage->getRequires()); - $package->setConflicts($curPackage->getConflicts()); - $package->setProvides($curPackage->getProvides()); - $package->setReplaces($curPackage->getReplaces()); - } elseif ($task === 'force-updates') { - if (($curPackage->getSourceReference() && $curPackage->getSourceReference() !== $package->getSourceReference()) - || ($curPackage->getDistReference() && $curPackage->getDistReference() !== $package->getDistReference()) - ) { - $operations[] = new UpdateOperation($package, $curPackage); - } - } - - break; - } - } - - continue; - } - - // find similar packages (name/version) in all repositories - $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion()), true); - foreach ($matches as $index => $match) { - // skip local packages - if (!in_array($match->getRepository(), $repositories, true)) { - unset($matches[$index]); - continue; - } - - $matches[$index] = $match->getId(); - } - - // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { - $newPackage = $pool->literalToPackage($matches[0]); - - if ($task === 'force-links' && $newPackage) { - $package->setRequires($newPackage->getRequires()); - $package->setConflicts($newPackage->getConflicts()); - $package->setProvides($newPackage->getProvides()); - $package->setReplaces($newPackage->getReplaces()); - } - - if ( - $task === 'force-updates' - && $newPackage - && ( - ($newPackage->getSourceReference() && $newPackage->getSourceReference() !== $package->getSourceReference()) - || ($newPackage->getDistReference() && $newPackage->getDistReference() !== $package->getDistReference()) - ) - ) { - $operations[] = new UpdateOperation($package, $newPackage); - - continue; - } - } - - if ($task === 'force-updates') { - // force installed package to update to referenced version in root package if it does not match the installed version - $references = $this->package->getReferences(); - - if (isset($references[$package->getName()]) && $references[$package->getName()] !== $package->getSourceReference()) { - // changing the source ref to update to will be handled in the operations loop - $operations[] = new UpdateOperation($package, clone $package); - } - } - } else { - // force update to locked version if it does not match the installed version - foreach ($lockedRepository->findPackages($package->getName()) as $lockedPackage) { - if ($lockedPackage->isDev() && $lockedPackage->getVersion() === $package->getVersion()) { - if ($task === 'force-links') { - $package->setRequires($lockedPackage->getRequires()); - $package->setConflicts($lockedPackage->getConflicts()); - $package->setProvides($lockedPackage->getProvides()); - $package->setReplaces($lockedPackage->getReplaces()); - } elseif ($task === 'force-updates') { - if (($lockedPackage->getSourceReference() && $lockedPackage->getSourceReference() !== $package->getSourceReference()) - || ($lockedPackage->getDistReference() && $lockedPackage->getDistReference() !== $package->getDistReference()) - ) { - $operations[] = new UpdateOperation($package, $lockedPackage); - } - } - - break; - } - } - } - } - - return $operations; - } - - /** - * Loads the most "current" list of packages that are installed meaning from lock ideally or from installed repo as fallback - * @param RepositoryInterface $installedRepo - * @return array - */ - private function getCurrentPackages($installedRepo) - { - if ($this->locker->isLocked()) { - try { - return $this->locker->getLockedRepository(true)->getPackages(); - } catch (\RuntimeException $e) { - // fetch only non-dev packages from lock if doing a dev update fails due to a previously incomplete lock file - return $this->locker->getLockedRepository()->getPackages(); - } - } - - return $installedRepo->getPackages(); - } - /** * @return array */ @@ -1120,58 +794,6 @@ class Installer return $normalizedAliases; } - /** - * @param Pool $pool - * @param PolicyInterface $policy - * @param WritableRepositoryInterface $localRepo - * @param array $repositories - */ - private function processPackageUrls(Pool $pool, $policy, $localRepo, $repositories) - { - if (!$this->update) { - return; - } - - $rootRefs = $this->package->getReferences(); - - foreach ($localRepo->getCanonicalPackages() as $package) { - // find similar packages (name/version) in pool - $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion()), true); - foreach ($matches as $index => $match) { - // skip local packages - if (!in_array($match->getRepository(), $repositories, true)) { - unset($matches[$index]); - continue; - } - - $matches[$index] = $match->getId(); - } - - // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { - $newPackage = $pool->literalToPackage($matches[0]); - - // update the dist and source URLs - $sourceUrl = $package->getSourceUrl(); - $newSourceUrl = $newPackage->getSourceUrl(); - $newReference = $newPackage->getSourceReference(); - - if ($package->isDev() && isset($rootRefs[$package->getName()]) && $package->getSourceReference() === $rootRefs[$package->getName()]) { - $newReference = $rootRefs[$package->getName()]; - } - - $this->updatePackageUrl($package, $newSourceUrl, $newPackage->getSourceType(), $newReference, $newPackage->getDistUrl()); - - if ($package instanceof CompletePackage && $newPackage instanceof CompletePackage) { - $package->setAbandoned($newPackage->getReplacementPackage() ?: $newPackage->isAbandoned()); - } - - $package->setDistMirrors($newPackage->getDistMirrors()); - $package->setSourceMirrors($newPackage->getSourceMirrors()); - } - } - } - private function updatePackageUrl(PackageInterface $package, $sourceUrl, $sourceType, $sourceReference, $distUrl) { $oldSourceRef = $package->getSourceReference(); @@ -1271,18 +893,14 @@ class Installer * skipped including their dependencies, unless they are listed in the * update whitelist themselves or $whitelistAllDependencies is true. * - * @param RepositoryInterface $localOrLockRepo Use the locked repo if available, otherwise installed repo will do + * @param RepositoryInterface $lockRepo Use the locked repo * 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($localOrLockRepo, array $rootRequires, array $rootDevRequires) + private function whitelistUpdateDependencies($lockRepo, array $rootRequires, array $rootDevRequires) { - if (!$this->updateWhitelist) { - return; - } - $rootRequires = array_merge($rootRequires, $rootDevRequires); $skipPackages = array(); @@ -1293,7 +911,7 @@ class Installer } $repositorySet = new RepositorySet(array(), 'dev'); - $repositorySet->addRepository($localOrLockRepo); + $repositorySet->addRepository($lockRepo); $seen = array(); @@ -1310,7 +928,7 @@ class Installer if (empty($depPackages)) { // add any installed package matching the whitelisted name/pattern $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); - foreach ($localOrLockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { + foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, false); } @@ -1418,12 +1036,12 @@ class Installer } /** - * @param RepositoryInterface $additionalInstalledRepository + * @param RepositoryInterface $additionalFixedRepository * @return $this */ - public function setAdditionalInstalledRepository(RepositoryInterface $additionalInstalledRepository) + public function setAdditionalFixedRepository(RepositoryInterface $additionalFixedRepository) { - $this->additionalInstalledRepository = $additionalInstalledRepository; + $this->additionalFixedRepository = $additionalFixedRepository; return $this; } diff --git a/src/Composer/Installer/SuggestedPackagesReporter.php b/src/Composer/Installer/SuggestedPackagesReporter.php index 25788e547..023aeb91d 100644 --- a/src/Composer/Installer/SuggestedPackagesReporter.php +++ b/src/Composer/Installer/SuggestedPackagesReporter.php @@ -96,21 +96,21 @@ class SuggestedPackagesReporter * @param RepositoryInterface $installedRepo Installed packages * @return SuggestedPackagesReporter */ - public function output(RepositoryInterface $installedRepo = null) + public function output(RepositoryInterface $lockedRepo = null) { $suggestedPackages = $this->getPackages(); - $installedPackages = array(); - if (null !== $installedRepo && ! empty($suggestedPackages)) { - foreach ($installedRepo->getPackages() as $package) { - $installedPackages = array_merge( - $installedPackages, + $lockedPackages = array(); + if (null !== $lockedRepo && ! empty($suggestedPackages)) { + foreach ($lockedRepo->getPackages() as $package) { + $lockedPackages = array_merge( + $lockedPackages, $package->getNames() ); } } foreach ($suggestedPackages as $suggestion) { - if (in_array($suggestion['target'], $installedPackages)) { + if (in_array($suggestion['target'], $lockedPackages)) { continue; } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 405d43261..063c893b6 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -40,6 +40,7 @@ class Locker private $dumper; private $process; private $lockDataCache; + private $virtualFileWritten; /** * Initializes packages locker. @@ -108,7 +109,7 @@ class Locker */ public function isLocked() { - if (!$this->lockFile->exists()) { + if (!$this->virtualFileWritten && !$this->lockFile->exists()) { return false; } @@ -282,10 +283,11 @@ class Locker * @param bool $preferStable * @param bool $preferLowest * @param array $platformOverrides + * @param bool $write Whether to actually write data to disk, useful in tests and for --dry-run * * @return bool */ - public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides) + public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides, $write = true) { $lock = array( '_readme' => array('This file locks the dependencies of your project to a known state', @@ -325,7 +327,11 @@ class Locker if (empty($lock['packages']) && empty($lock['packages-dev']) && empty($lock['platform']) && empty($lock['platform-dev'])) { if ($this->lockFile->exists()) { - unlink($this->lockFile->getPath()); + if ($write) { + unlink($this->lockFile->getPath()); + } else { + $this->virtualFileWritten = false; + } } return false; @@ -337,8 +343,15 @@ class Locker $isLocked = false; } if (!$isLocked || $lock !== $this->getLockData()) { - $this->lockFile->write($lock); - $this->lockDataCache = null; + if ($write) { + $this->lockFile->write($lock); +// $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT)); + $this->lockDataCache = null; + $this->virtualFileWritten = false; + } else { + $this->virtualFileWritten = true; + $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT)); + } return true; } diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 7314c6540..919d098e3 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -29,7 +29,7 @@ class DefaultPolicyTest extends TestCase /** @var ArrayRepository */ protected $repo; /** @var ArrayRepository */ - protected $repoInstalled; + protected $repoLocked; /** @var DefaultPolicy */ protected $policy; @@ -37,7 +37,7 @@ class DefaultPolicyTest extends TestCase { $this->repositorySet = new RepositorySet(array(), 'dev'); $this->repo = new ArrayRepository; - $this->repoInstalled = new ArrayRepository; + $this->repoLocked = new ArrayRepository; $this->policy = new DefaultPolicy; } @@ -52,7 +52,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId()); $expected = array($packageA->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -68,7 +68,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -84,7 +84,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -101,7 +101,7 @@ class DefaultPolicyTest extends TestCase $expected = array($packageA1->getId()); $policy = new DefaultPolicy(true); - $selected = $policy->selectPreferredPackages($pool, array(), $literals); + $selected = $policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -117,24 +117,24 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } - public function testSelectNewestOverInstalled() + public function testSelectNewestOverLocked() { $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); - $this->repoInstalled->addPackage($packageAInstalled = $this->getPackage('A', '1.0')); - $this->repositorySet->addRepository($this->repoInstalled); + $this->repoLocked->addPackage($packageAInstalled = $this->getPackage('A', '1.0')); $this->repositorySet->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoLocked); $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA->getId(), $packageAInstalled->getId()); $expected = array($packageA->getId()); - $selected = $this->policy->selectPreferredPackages($pool, $this->mapFromRepo($this->repoInstalled), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -146,16 +146,16 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $otherRepository->addPackage($packageAImportant = $this->getPackage('A', '1.0')); - $this->repositorySet->addRepository($this->repoInstalled); $this->repositorySet->addRepository($otherRepository); $this->repositorySet->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoLocked); $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA->getId(), $packageAImportant->getId()); $expected = array($packageAImportant->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -177,7 +177,7 @@ class DefaultPolicyTest extends TestCase $literals = array($package1->getId(), $package2->getId(), $package3->getId(), $package4->getId()); $expected = array($package2->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); @@ -188,7 +188,7 @@ class DefaultPolicyTest extends TestCase $pool = $this->repositorySet->createPoolForPackage('A'); $expected = array($package4->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -205,9 +205,9 @@ class DefaultPolicyTest extends TestCase $repoImportant->addPackage($packageA2AliasImportant = new AliasPackage($packageA2Important, '2.1.9999999.9999999-dev', '2.1.x-dev')); $packageAAliasImportant->setRootPackageAlias(true); - $this->repositorySet->addRepository($this->repoInstalled); $this->repositorySet->addRepository($repoImportant); $this->repositorySet->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoLocked); $pool = $this->repositorySet->createPoolForPackage('A'); @@ -219,7 +219,7 @@ class DefaultPolicyTest extends TestCase $expected = array($packageAAliasImportant->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -239,7 +239,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -258,7 +258,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -279,7 +279,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals, 'vendor-a/package'); + $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package'); $this->assertEquals($expected, $selected); // test with reversed order in repo @@ -295,20 +295,10 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals, 'vendor-a/package'); + $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package'); $this->assertSame($expected, $selected); } - protected function mapFromRepo(RepositoryInterface $repo) - { - $map = array(); - foreach ($repo->getPackages() as $package) { - $map[$package->getId()] = true; - } - - return $map; - } - public function testSelectLowest() { $policy = new DefaultPolicy(false, true); @@ -322,7 +312,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); - $selected = $policy->selectPreferredPackages($pool, array(), $literals); + $selected = $policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index dfa411ed9..405f3b0c8 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -31,14 +31,12 @@ class RequestTest extends TestCase $request = new Request(); $request->install('foo'); - $request->fix('bar'); $request->remove('foobar'); $this->assertEquals( array( - array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null, 'fixed' => false), - array('cmd' => 'install', 'packageName' => 'bar', 'constraint' => null, 'fixed' => true), - array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null, 'fixed' => false), + array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null), + array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null), ), $request->getJobs() ); @@ -60,21 +58,9 @@ class RequestTest extends TestCase $this->assertEquals( array( - array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint, 'fixed' => false), + array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint), ), $request->getJobs() ); } - - public function testUpdateAll() - { - $request = new Request(); - - $request->updateAll(); - - $this->assertEquals( - array(array('cmd' => 'update-all')), - $request->getJobs() - ); - } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index b80e7c61d..54f726ef1 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -30,6 +30,7 @@ class SolverTest extends TestCase protected $repoSet; protected $repo; protected $repoInstalled; + protected $repoLocked; protected $request; protected $policy; protected $solver; @@ -39,6 +40,7 @@ class SolverTest extends TestCase $this->repoSet = new RepositorySet(array()); $this->repo = new ArrayRepository; $this->repoInstalled = new InstalledArrayRepository; + $this->repoLocked = new ArrayRepository; $this->request = new Request($this->repoSet); $this->policy = new DefaultPolicy; @@ -59,6 +61,7 @@ class SolverTest extends TestCase public function testSolverRemoveIfNotInstalled() { $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage(clone $packageA); $this->reposComplete(); $this->checkSolverResult(array( @@ -919,8 +922,8 @@ class SolverTest extends TestCase protected function reposComplete() { - $this->repoSet->addRepository($this->repoInstalled); $this->repoSet->addRepository($this->repo); + $this->repoSet->addRepository($this->repoLocked); } protected function createSolver() diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index d774ea188..ec108c7b9 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -95,6 +95,55 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" } } ] +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --EXPECT-LOCK-- { "packages": [ diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index acaf8f1ff..05c49f6a9 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -16,6 +16,7 @@ use Composer\Installer; use Composer\Console\Application; use Composer\IO\BufferIO; use Composer\Json\JsonFile; +use Composer\Package\Dumper\ArrayDumper; use Composer\Util\Filesystem; use Composer\Repository\ArrayRepository; use Composer\Repository\RepositoryManager; @@ -74,10 +75,30 @@ class InstallerTest extends TestCase foreach ($repositories as $repository) { $repositoryManager->addRepository($repository); } - - $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock(); $installationManager = new InstallationManagerMock(); + // emulate a writable lock file + $lockData = null; + $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); + $lockJsonMock->expects($this->any()) + ->method('read') + ->will($this->returnCallback(function() use (&$lockData) { + return json_decode($lockData, true); + })); + $lockJsonMock->expects($this->any()) + ->method('exists') + ->will($this->returnCallback(function () use (&$lockData) { + return $lockData !== null; + })); + $lockJsonMock->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { + $lockData = json_encode($value, JSON_PRETTY_PRINT); + })); + + $tempLockData = null; + $locker = new Locker($io, $lockJsonMock, $repositoryManager, $installationManager, '{}'); + $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator); @@ -91,7 +112,7 @@ class InstallerTest extends TestCase $expectedUninstalled = isset($options['uninstall']) ? $options['uninstall'] : array(); $installed = $installationManager->getInstalledPackages(); - $this->assertSame($expectedInstalled, $installed); + $this->assertEquals($this->makePackagesComparable($expectedInstalled), $this->makePackagesComparable($installed)); $updated = $installationManager->getUpdatedPackages(); $this->assertSame($expectedUpdated, $updated); @@ -100,6 +121,17 @@ class InstallerTest extends TestCase $this->assertSame($expectedUninstalled, $uninstalled); } + protected function makePackagesComparable($packages) + { + $dumper = new ArrayDumper(); + + $comparable = []; + foreach ($packages as $package) { + $comparable[] = $dumper->dump($package); + } + return $comparable; + } + public function provideInstaller() { $cases = array(); @@ -109,11 +141,11 @@ class InstallerTest extends TestCase $a = $this->getPackage('A', '1.0.0', 'Composer\Package\RootPackage'); $a->setRequires(array( - new Link('A', 'B', $this->getVersionConstraint('=', '1.0.0')), + 'b' => new Link('A', 'B', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $b = $this->getPackage('B', '1.0.0'); $b->setRequires(array( - new Link('B', 'A', $this->getVersionConstraint('=', '1.0.0')), + 'a' => new Link('B', 'A', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $cases[] = array( @@ -129,11 +161,11 @@ class InstallerTest extends TestCase $a = $this->getPackage('A', '1.0.0', 'Composer\Package\RootPackage'); $a->setRequires(array( - new Link('A', 'B', $this->getVersionConstraint('=', '1.0.0')), + 'b' => new Link('A', 'B', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $b = $this->getPackage('B', '1.0.0'); $b->setRequires(array( - new Link('B', 'A', $this->getVersionConstraint('=', '1.0.0')), + 'a' => new Link('B', 'A', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $cases[] = array( @@ -144,6 +176,7 @@ class InstallerTest extends TestCase ), ); + // TODO why are there not more cases with uninstall/update? return $cases; } @@ -182,13 +215,24 @@ class InstallerTest extends TestCase $repositoryManager = $composer->getRepositoryManager(); $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); + // emulate a writable lock file + $lockData = $lock ? json_encode($lock, JSON_PRETTY_PRINT): null; $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); $lockJsonMock->expects($this->any()) ->method('read') - ->will($this->returnValue($lock)); + ->will($this->returnCallback(function() use (&$lockData) { + return json_decode($lockData, true); + })); $lockJsonMock->expects($this->any()) ->method('exists') - ->will($this->returnValue(true)); + ->will($this->returnCallback(function () use (&$lockData) { + return $lockData !== null; + })); + $lockJsonMock->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { + $lockData = json_encode($value, JSON_PRETTY_PRINT); + })); if ($expectLock) { $actualLock = array(); @@ -245,7 +289,7 @@ class InstallerTest extends TestCase $application->setAutoExit(false); $appOutput = fopen('php://memory', 'w+'); - $input = new StringInput($run); + $input = new StringInput($run.' -vvv'); $input->setInteractive(false); $result = $application->run($input, new StreamOutput($appOutput)); fseek($appOutput, 0); From c875f538ea49048a783d43082e3dfeb0a3d5c5dc Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 14 Feb 2019 17:57:29 +0100 Subject: [PATCH 062/321] Make root alias behaviour consistent, add root ref handling, lock to newest metadata root aliases during install should come from the lock file only, for better reproducibility we don't reuse the value from update for the following install --- .../DependencyResolver/LockTransaction.php | 10 +++- src/Composer/Installer.php | 59 +++++++++---------- src/Composer/Repository/RepositorySet.php | 2 + 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 35cf760b9..ad25f8fc1 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -108,7 +108,7 @@ class LockTransaction } // TODO additionalFixedRepository needs to be looked at here as well? - public function getNewLockNonDevPackages() + public function getNewLockNonDevPackages(array $rootForceReferences) { $packages = array(); foreach ($this->decisions as $i => $decision) { @@ -117,6 +117,14 @@ class LockTransaction if ($literal > 0) { $package = $this->pool->literalToPackage($literal); if (!isset($this->unlockableMap[$package->id]) && !($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + + echo "rootRef? ".$package->getName()."\n"; + // TODO can we really just do this for all of them here? What if we're doing a partial update, should this change anyway? + if (isset($rootForceReferences[$package->getName()])) { + echo "rootRef! ".$package->getName()."\n"; + $package->setSourceReference($rootForceReferences[$package->getName()]); + } + $packages[] = $package; } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 07433c911..ffeb4ee98 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -219,21 +219,17 @@ class Installer $this->downloadManager->setPreferDist($this->preferDist); $localRepo = $this->repositoryManager->getLocalRepository(); - $platformRepo = $this->createPlatformRepo($this->update); - - $aliases = $this->getRootAliases(); - $this->aliasPlatformPackages($platformRepo, $aliases); if (!$this->suggestedPackagesReporter) { $this->suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); } try { - // TODO what are installs? does locking a package without downloading code count? if ($this->update) { - $res = $this->doUpdate($localRepo, $platformRepo, $aliases, true); + // TODO introduce option to set doInstall to false (update lock file without vendor install) + $res = $this->doUpdate($localRepo, true); } else { - $res = $this->doInstall($localRepo, $platformRepo, $aliases, false); + $res = $this->doInstall($localRepo); } if ($res !== 0) { return $res; @@ -317,8 +313,11 @@ class Installer return 0; } - protected function doUpdate(RepositoryInterface $localRepo, PlatformRepository $platformRepo, $aliases, $doInstall) + protected function doUpdate(RepositoryInterface $localRepo, $doInstall) { + $platformRepo = $this->createPlatformRepo(true); + $aliases = $this->getRootAliases(true); + $lockedRepository = null; if ($this->locker->isLocked()) { @@ -377,6 +376,13 @@ class Installer if ($this->updateWhitelist) { foreach ($lockedRepository->getPackages() as $lockedPackage) { if (!$this->isUpdateable($lockedPackage) && $repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { + // need to actually allow for metadata updates at all times, so we want to fix the most recent prefered package in the repo set instead + $packages = $repositorySet->findPackages($lockedPackage->getName(), new Constraint('=', $lockedPackage->getVersion())); + $lockedPackage = isset($packages[0]) ? $packages[0] : $lockedPackage; + + // in how far do we need to reset requirements here, theoretically it's the same version so nothing should have changed, but for a dev version it could have? + + // TODO add reason for fix? $request->fixPackage($lockedPackage); } @@ -418,7 +424,7 @@ class Installer $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockNonDevPackages(), + $lockTransaction->getNewLockNonDevPackages($this->package->getReferences()), $lockTransaction->getNewLockDevPackages(), $platformReqs, $platformDevReqs, @@ -473,22 +479,6 @@ class Installer $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); } - // TODO should this really happen here or should this be written to the lock file? - // updating, force dev packages' references if they're in root package refs - $package = null; - if ('update' === $jobType) { - $package = $operation->getTargetPackage(); - } elseif ('install' === $jobType) { - $package = $operation->getPackage(); - } - - if ($package && $package->isDev()) { - $references = $this->package->getReferences(); - if (isset($references[$package->getName()])) { - $this->updateInstallReferences($package, $references[$package->getName()]); - } - } - // output op, but alias op only in debug verbosity if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation); @@ -514,7 +504,7 @@ class Installer if ($doInstall) { // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false - return $this->doInstall($localRepo, $platformRepo, $aliases, true); + return $this->doInstall($localRepo, true); } return 0; @@ -527,8 +517,11 @@ class Installer * @param array $aliases * @return int exit code */ - protected function doInstall(RepositoryInterface $localRepo, PlatformRepository $platformRepo, $aliases, $alreadySolved = false) + protected function doInstall(RepositoryInterface $localRepo, $alreadySolved = false) { + $platformRepo = $this->createPlatformRepo(false); + $aliases = $this->getRootAliases(false); + $lockedRepository = $this->locker->getLockedRepository($this->devMode); // creating repository set @@ -669,6 +662,8 @@ class Installer */ private function createRepositorySet(PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) { + $this->aliasPlatformPackages($platformRepo, $rootAliases); + // TODO what's the point of rootConstraints at all, we generate the package pool taking them into account anyway? // TODO maybe we can drop the lockedRepository here if ($this->update) { @@ -772,11 +767,12 @@ class Installer } /** + * @param bool $forUpdate * @return array */ - private function getRootAliases() + private function getRootAliases($forUpdate) { - if ($this->update) { + if ($forUpdate) { $aliases = $this->package->getAliases(); } else { $aliases = $this->locker->getAliases(); @@ -838,9 +834,10 @@ class Installer */ private function aliasPlatformPackages(PlatformRepository $platformRepo, $aliases) { - foreach ($aliases as $package => $versions) { + // TODO should the repository set do this? + foreach ($aliases as $packageName => $versions) { foreach ($versions as $version => $alias) { - $packages = $platformRepo->findPackages($package, $version); + $packages = $platformRepo->findPackages($packageName, $version); foreach ($packages as $package) { $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); $aliasPackage->setRootPackageAlias(true); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index bac63dd36..6a232630d 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -101,6 +101,8 @@ class RepositorySet /** * Find packages providing or matching a name and optionally meeting a constraint in all repositories * + * Returned in the order of repositories, matching priority + * * @param string $name * @param ConstraintInterface|null $constraint * @param bool $exactMatch From 0fd74d1cc43ce7bedfc21e473ea4b44e9b3a2503 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 12:28:21 +0100 Subject: [PATCH 063/321] Fix PerforceDownloader, fixes #7979 --- src/Composer/Downloader/PerforceDownloader.php | 2 +- tests/Composer/Test/Downloader/PerforceDownloaderTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/Downloader/PerforceDownloader.php b/src/Composer/Downloader/PerforceDownloader.php index df270417f..a7dc013b3 100644 --- a/src/Composer/Downloader/PerforceDownloader.php +++ b/src/Composer/Downloader/PerforceDownloader.php @@ -78,7 +78,7 @@ class PerforceDownloader extends VcsDownloader */ public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { - $this->doDownload($target, $path, $url); + $this->doInstall($target, $path, $url); } /** diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php index d2b8fb753..77a61f9cf 100644 --- a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php +++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php @@ -121,7 +121,7 @@ class PerforceDownloaderTest extends TestCase * @depends testInitPerforceInstantiatesANewPerforceObject * @depends testInitPerforceDoesNothingIfPerforceAlreadySet */ - public function testDoDownloadWithTag() + public function testDoInstallWithTag() { //I really don't like this test but the logic of each Perforce method is tested in the Perforce class. Really I am just enforcing workflow. $ref = 'SOURCE_REF@123'; @@ -145,7 +145,7 @@ class PerforceDownloaderTest extends TestCase * @depends testInitPerforceInstantiatesANewPerforceObject * @depends testInitPerforceDoesNothingIfPerforceAlreadySet */ - public function testDoDownloadWithNoTag() + public function testDoInstallWithNoTag() { $ref = 'SOURCE_REF'; $label = null; From fb3d0981c04570d283485da984f4d4d96a998b78 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 12:35:24 +0100 Subject: [PATCH 064/321] Fix test suite --- tests/Composer/Test/DependencyResolver/SolverTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index b80e7c61d..e50927fe2 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -899,6 +899,8 @@ class SolverTest extends TestCase $this->request->install('A'); + $this->createSolver(); + // check correct setup for assertion later $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral); From a062cd1a31f05dd5b87f761f1784dec4775f7306 Mon Sep 17 00:00:00 2001 From: CZechBoY Date: Mon, 7 Jan 2019 16:22:41 +0100 Subject: [PATCH 065/321] added phpstan on level 0 --- .travis.yml | 12 +++++- phpstan/autoload.php | 5 +++ phpstan/config.neon | 38 +++++++++++++++++++ .../DependencyResolver/PoolBuilder.php | 6 +-- src/Composer/Downloader/GzipDownloader.php | 1 + src/Composer/Downloader/RarDownloader.php | 1 + src/Composer/Downloader/XzDownloader.php | 1 + src/Composer/Downloader/ZipDownloader.php | 2 + src/Composer/Package/Locker.php | 8 ++++ .../Package/Version/VersionGuesser.php | 1 + src/Composer/Util/Bitbucket.php | 6 +++ src/Composer/Util/Filesystem.php | 4 ++ src/Composer/Util/GitHub.php | 4 ++ src/Composer/Util/GitLab.php | 4 ++ src/Composer/Util/Loop.php | 2 +- src/Composer/Util/StreamContextFactory.php | 1 + .../Test/Downloader/FileDownloaderTest.php | 2 + .../Test/Downloader/ZipDownloaderTest.php | 2 +- 18 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 phpstan/autoload.php create mode 100644 phpstan/config.neon diff --git a/.travis.yml b/.travis.yml index f02fefcb1..1140053c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,10 +23,15 @@ matrix: - php: 5.6 - php: 7.0 - php: 7.1 + env: PHPSTAN=1 - php: 7.2 + env: PHPSTAN=1 - php: 7.3 + env: PHPSTAN=1 - php: 7.3 - env: deps=high + env: + - deps=high + - PHPSTAN=1 - php: nightly fast_finish: true allow_failures: @@ -58,6 +63,11 @@ before_script: script: # run test suite directories in parallel using GNU parallel - ls -d tests/Composer/Test/* | grep -v TestCase.php | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml --colors=always {} || (echo -e "\e[41mFAILED\e[0m {}" && exit 1);' + # Run PHPStan + - if [[ $PHPSTAN == "1" ]]; then + composer require --dev phpstan/phpstan-shim:^0.11 --ignore-platform-reqs && + vendor/bin/phpstan.phar analyse src tests --configuration=phpstan/config.neon --autoload-file=phpstan/autoload.php; + fi before_deploy: - php -d phar.readonly=0 bin/compile diff --git a/phpstan/autoload.php b/phpstan/autoload.php new file mode 100644 index 000000000..7d1ed7671 --- /dev/null +++ b/phpstan/autoload.php @@ -0,0 +1,5 @@ +pool = new Pool($this->filterRequires); + $pool = new Pool($this->filterRequires); $this->rootAliases = $rootAliases; // TODO do we really want the request here? kind of want a root requirements thingy instead @@ -133,13 +133,13 @@ class PoolBuilder } } - $this->pool->setPackages($this->packages, $this->priorities); + $pool->setPackages($this->packages, $this->priorities); unset($this->aliasMap); unset($this->loadedNames); unset($this->nameConstraints); - return $this->pool; + return $pool; } private function loadPackage(PackageInterface $package, $repoIndex) diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index 9748b91ac..91be4593d 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -28,6 +28,7 @@ use Composer\IO\IOInterface; */ class GzipDownloader extends ArchiveDownloader { + /** @var ProcessExecutor */ protected $process; public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index 2ebc3bf18..d0fbadcc6 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -32,6 +32,7 @@ use RarArchive; */ class RarDownloader extends ArchiveDownloader { + /** @var ProcessExecutor */ protected $process; public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 19e51c321..371ceda1b 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -28,6 +28,7 @@ use Composer\IO\IOInterface; */ class XzDownloader extends ArchiveDownloader { + /** @var ProcessExecutor */ protected $process; public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index efa9fc994..bd8d3b499 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -33,7 +33,9 @@ class ZipDownloader extends ArchiveDownloader private static $hasZipArchive; private static $isWindows; + /** @var ProcessExecutor */ protected $process; + /** @var ZipArchive|null */ private $zipArchiveObject; public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 405d43261..68540581c 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -31,13 +31,21 @@ use Seld\JsonLint\ParsingException; */ class Locker { + /** @var JsonFile */ private $lockFile; + /** @var RepositoryManager */ private $repositoryManager; + /** @var InstallationManager */ private $installationManager; + /** @var string */ private $hash; + /** @var string */ private $contentHash; + /** @var ArrayLoader */ private $loader; + /** @var ArrayDumper */ private $dumper; + /** @var ProcessExecutor */ private $process; private $lockDataCache; diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 1c2fdf986..d655bc080 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -17,6 +17,7 @@ use Composer\Repository\Vcs\HgDriver; use Composer\IO\NullIO; use Composer\Semver\VersionParser as SemverVersionParser; use Composer\Util\Git as GitUtil; +use Composer\Util\HttpDownloader; use Composer\Util\ProcessExecutor; use Composer\Util\Svn as SvnUtil; diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index d9f569b1b..ea02dfa0b 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -22,11 +22,17 @@ use Composer\Downloader\TransportException; */ class Bitbucket { + /** @var IOInterface */ private $io; + /** @var Config */ private $config; + /** @var ProcessExecutor */ private $process; + /** @var HttpDownloader */ private $httpDownloader; + /** @var array */ private $token = array(); + /** @var int|null */ private $time; const OAUTH2_ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token'; diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 1903f1c8d..805eda14b 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -23,6 +23,7 @@ use Symfony\Component\Finder\Finder; */ class Filesystem { + /** @var ProcessExecutor */ private $processExecutor; public function __construct(ProcessExecutor $executor = null) @@ -537,6 +538,9 @@ class Filesystem return $size; } + /** + * @return ProcessExecutor + */ protected function getProcess() { return $this->processExecutor; diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index c3046cb77..d6bfb62ca 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -22,9 +22,13 @@ use Composer\Downloader\TransportException; */ class GitHub { + /** @var IOInterface */ protected $io; + /** @var Config */ protected $config; + /** @var ProcessExecutor */ protected $process; + /** @var HttpDownloader */ protected $httpDownloader; /** diff --git a/src/Composer/Util/GitLab.php b/src/Composer/Util/GitLab.php index 2a4867954..b2dbf2836 100644 --- a/src/Composer/Util/GitLab.php +++ b/src/Composer/Util/GitLab.php @@ -23,9 +23,13 @@ use Composer\Json\JsonFile; */ class GitLab { + /** @var IOInterface */ protected $io; + /** @var Config */ protected $config; + /** @var ProcessExecutor */ protected $process; + /** @var HttpDownloader */ protected $httpDownloader; /** diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index 1be7d478b..c50cf4b02 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -20,7 +20,7 @@ use React\Promise\Promise; */ class Loop { - private $io; + private $httpDownloader; public function __construct(HttpDownloader $httpDownloader) { diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index a87bc6d8b..f39c6dd38 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -14,6 +14,7 @@ namespace Composer\Util; use Composer\Composer; use Composer\CaBundle\CaBundle; +use Composer\Downloader\TransportException; use Psr\Log\LoggerInterface; /** diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index 10ea401a8..89c538eab 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -20,6 +20,8 @@ use Composer\Util\Loop; class FileDownloaderTest extends TestCase { + private $httpDownloader; + protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $httpDownloader = null, $filesystem = null) { $io = $io ?: $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index b754af607..e69149271 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -25,7 +25,7 @@ class ZipDownloaderTest extends TestCase * @var string */ private $testDir; - private $prophet; + private $httpDownloader; private $io; private $config; private $package; From 799717f102ec64482d2aaaa5fef58bd86952e9ad Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 13:03:34 +0100 Subject: [PATCH 066/321] Tweak and fix some more phpstan reports --- phpstan/config.neon | 6 ++++-- .../Test/DependencyResolver/SolverTest.php | 2 +- .../Test/Downloader/PerforceDownloaderTest.php | 4 ++-- .../Test/Repository/Vcs/PerforceDriverTest.php | 2 +- .../Test/Repository/VcsRepositoryTest.php | 15 +++++++++------ 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/phpstan/config.neon b/phpstan/config.neon index 5f77d5cd1..7c0122f9a 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -29,9 +29,11 @@ parameters: - '~^Undefined variable: \$vendorDir$~' - '~^Undefined variable: \$baseDir$~' + # variable defined in eval + - '~^Undefined variable: \$res$~' + # always checked whether the class exists - - '~^Instantiated class Symfony\\Component\\Console\\Terminal not found\.$~' - - '~^Class Symfony\\Component\\Console\\Input\\StreamableInputInterface not found\.$~' + - '~^Call to an undefined static method Symfony\\Component\\Process\\Process::fromShellCommandline\(\).$~' # parent call in test mocks - '~^Composer\\Test\\Mock\\HttpDownloaderMock::__construct\(\) does not call parent constructor from Composer\\Util\\HttpDownloader\.$~' diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index e50927fe2..95696e610 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -40,7 +40,7 @@ class SolverTest extends TestCase $this->repo = new ArrayRepository; $this->repoInstalled = new InstalledArrayRepository; - $this->request = new Request($this->repoSet); + $this->request = new Request(); $this->policy = new DefaultPolicy; } diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php index 77a61f9cf..b6347789b 100644 --- a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php +++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php @@ -129,7 +129,7 @@ class PerforceDownloaderTest extends TestCase $this->package->expects($this->once())->method('getSourceReference')->will($this->returnValue($ref)); $this->io->expects($this->once())->method('writeError')->with($this->stringContains('Cloning '.$ref)); $perforceMethods = array('setStream', 'p4Login', 'writeP4ClientSpec', 'connectClient', 'syncCodeBase', 'cleanupClientSpec'); - $perforce = $this->getMockBuilder('Composer\Util\Perforce', $perforceMethods)->disableOriginalConstructor()->getMock(); + $perforce = $this->getMockBuilder('Composer\Util\Perforce')->disableOriginalConstructor()->getMock(); $perforce->expects($this->at(0))->method('initializePath')->with($this->equalTo($this->testPath)); $perforce->expects($this->at(1))->method('setStream')->with($this->equalTo($ref)); $perforce->expects($this->at(2))->method('p4Login'); @@ -152,7 +152,7 @@ class PerforceDownloaderTest extends TestCase $this->package->expects($this->once())->method('getSourceReference')->will($this->returnValue($ref)); $this->io->expects($this->once())->method('writeError')->with($this->stringContains('Cloning '.$ref)); $perforceMethods = array('setStream', 'p4Login', 'writeP4ClientSpec', 'connectClient', 'syncCodeBase', 'cleanupClientSpec'); - $perforce = $this->getMockBuilder('Composer\Util\Perforce', $perforceMethods)->disableOriginalConstructor()->getMock(); + $perforce = $this->getMockBuilder('Composer\Util\Perforce')->disableOriginalConstructor()->getMock(); $perforce->expects($this->at(0))->method('initializePath')->with($this->equalTo($this->testPath)); $perforce->expects($this->at(1))->method('setStream')->with($this->equalTo($ref)); $perforce->expects($this->at(2))->method('p4Login'); diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php index ff4e19121..1c44e82dd 100644 --- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -108,7 +108,7 @@ class PerforceDriverTest extends TestCase { $methods = array('p4login', 'checkStream', 'writeP4ClientSpec', 'connectClient', 'getComposerInformation', 'cleanupClientSpec'); - return $this->getMockBuilder('Composer\Util\Perforce', $methods)->disableOriginalConstructor()->getMock(); + return $this->getMockBuilder('Composer\Util\Perforce')->disableOriginalConstructor()->getMock(); } public function testInitializeCapturesVariablesFromRepoConfig() diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php index 0cab2f8bb..65bf52409 100644 --- a/tests/Composer/Test/Repository/VcsRepositoryTest.php +++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php @@ -32,17 +32,18 @@ class VcsRepositoryTest extends TestCase protected function initialize() { - $oldCwd = getcwd(); - self::$composerHome = $this->getUniqueTmpDirectory(); - self::$gitRepo = $this->getUniqueTmpDirectory(); - $locator = new ExecutableFinder(); if (!$locator->find('git')) { $this->skipped = 'This test needs a git binary in the PATH to be able to run'; return; } - if (!@mkdir(self::$gitRepo) || !@chdir(self::$gitRepo)) { + + $oldCwd = getcwd(); + self::$composerHome = $this->getUniqueTmpDirectory(); + self::$gitRepo = $this->getUniqueTmpDirectory(); + + if (!@chdir(self::$gitRepo)) { $this->skipped = 'Could not create and move into the temp git repo '.self::$gitRepo; return; @@ -60,6 +61,7 @@ class VcsRepositoryTest extends TestCase $exec('git init'); $exec('git config user.email composertest@example.org'); $exec('git config user.name ComposerTest'); + $exec('git config commit.gpgsign false'); touch('foo'); $exec('git add foo'); $exec('git commit -m init'); @@ -150,7 +152,8 @@ class VcsRepositoryTest extends TestCase 'home' => self::$composerHome, ), )); - $repo = new VcsRepository(array('url' => self::$gitRepo, 'type' => 'vcs'), new NullIO, $config); + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); + $repo = new VcsRepository(array('url' => self::$gitRepo, 'type' => 'vcs'), new NullIO, $config, $httpDownloader); $packages = $repo->getPackages(); $dumper = new ArrayDumper(); From 2c520bf93b985a33ab7c5f3ca281142ea00ca256 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 13:37:30 +0100 Subject: [PATCH 067/321] Fix build --- phpstan/config.neon | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpstan/config.neon b/phpstan/config.neon index 7c0122f9a..9fd155963 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -33,6 +33,8 @@ parameters: - '~^Undefined variable: \$res$~' # always checked whether the class exists + - '~^Instantiated class Symfony\\Component\\Console\\Terminal not found\.$~' + - '~^Class Symfony\\Component\\Console\\Input\\StreamableInputInterface not found\.$~' - '~^Call to an undefined static method Symfony\\Component\\Process\\Process::fromShellCommandline\(\).$~' # parent call in test mocks From 9da40b3c2c000ce36ddb74b9afd80e8ab77333d4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 13:56:08 +0100 Subject: [PATCH 068/321] Only run phpstan once --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1140053c0..a06922904 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,15 +23,12 @@ matrix: - php: 5.6 - php: 7.0 - php: 7.1 - env: PHPSTAN=1 - php: 7.2 - env: PHPSTAN=1 - php: 7.3 env: PHPSTAN=1 - php: 7.3 env: - deps=high - - PHPSTAN=1 - php: nightly fast_finish: true allow_failures: From 0caa616e6c498e0ffb4a6363d0ee695698956068 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 14:03:23 +0100 Subject: [PATCH 069/321] Fix test warnings --- tests/Composer/Test/ApplicationTest.php | 2 +- tests/Composer/Test/CacheTest.php | 6 ++++-- .../Test/Downloader/FileDownloaderTest.php | 8 ++++---- .../Test/Downloader/GitDownloaderTest.php | 16 ++++++++-------- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/Composer/Test/ApplicationTest.php b/tests/Composer/Test/ApplicationTest.php index 8739a5004..39d462544 100644 --- a/tests/Composer/Test/ApplicationTest.php +++ b/tests/Composer/Test/ApplicationTest.php @@ -47,7 +47,7 @@ class ApplicationTest extends TestCase $index = 0; $outputMock->expects($this->at($index++)) - ->method("writeError"); + ->method("write"); if (extension_loaded('xdebug')) { $outputMock->expects($this->at($index++)) diff --git a/tests/Composer/Test/CacheTest.php b/tests/Composer/Test/CacheTest.php index 50c767752..a8d3eae4a 100644 --- a/tests/Composer/Test/CacheTest.php +++ b/tests/Composer/Test/CacheTest.php @@ -20,6 +20,7 @@ class CacheTest extends TestCase private $files; private $root; private $finder; + private $filesystem; private $cache; public function setUp() @@ -34,11 +35,12 @@ class CacheTest extends TestCase } $this->finder = $this->getMockBuilder('Symfony\Component\Finder\Finder')->disableOriginalConstructor()->getMock(); + $this->filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $this->cache = $this->getMockBuilder('Composer\Cache') ->setMethods(array('getFinder')) - ->setConstructorArgs(array($io, $this->root)) + ->setConstructorArgs(array($io, $this->root, 'a-z0-9.', $this->filesystem)) ->getMock(); $this->cache ->expects($this->any()) @@ -105,7 +107,7 @@ class CacheTest extends TestCase public function testClearCache() { - $this->finder + $this->filesystem ->method('removeDirectory') ->with($this->root) ->willReturn(true); diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index 89c538eab..7c4259ca3 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -223,7 +223,7 @@ class FileDownloaderTest extends TestCase public function testDowngradeShowsAppropriateMessage() { - $oldPackage = $this->getMock('Composer\Package\PackageInterface'); + $oldPackage = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $oldPackage->expects($this->any()) ->method('getFullPrettyVersion') ->will($this->returnValue('1.2.0')); @@ -231,7 +231,7 @@ class FileDownloaderTest extends TestCase ->method('getVersion') ->will($this->returnValue('1.2.0.0')); - $newPackage = $this->getMock('Composer\Package\PackageInterface'); + $newPackage = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $newPackage->expects($this->any()) ->method('getFullPrettyVersion') ->will($this->returnValue('1.0.0')); @@ -245,7 +245,7 @@ class FileDownloaderTest extends TestCase ->method('getDistUrls') ->will($this->returnValue(array($distUrl))); - $ioMock = $this->getMock('Composer\IO\IOInterface'); + $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $ioMock->expects($this->at(0)) ->method('writeError') ->with($this->stringContains('Downloading')); @@ -256,7 +256,7 @@ class FileDownloaderTest extends TestCase $path = $this->getUniqueTmpDirectory(); touch($path.'_script.js'); - $filesystem = $this->getMock('Composer\Util\Filesystem'); + $filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); $filesystem->expects($this->once()) ->method('removeDirectory') ->will($this->returnValue(true)); diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index b5d0054de..b9a85a666 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -604,7 +604,7 @@ composer https://github.com/old/url (push) public function testDowngradeShowsAppropriateMessage() { - $oldPackage = $this->getMock('Composer\Package\PackageInterface'); + $oldPackage = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $oldPackage->expects($this->any()) ->method('getVersion') ->will($this->returnValue('1.2.0.0')); @@ -618,7 +618,7 @@ composer https://github.com/old/url (push) ->method('getSourceUrls') ->will($this->returnValue(array('/foo/bar', 'https://github.com/composer/composer'))); - $newPackage = $this->getMock('Composer\Package\PackageInterface'); + $newPackage = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $newPackage->expects($this->any()) ->method('getSourceReference') ->will($this->returnValue('ref')); @@ -632,12 +632,12 @@ composer https://github.com/old/url (push) ->method('getFullPrettyVersion') ->will($this->returnValue('1.0.0')); - $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor->expects($this->any()) ->method('execute') ->will($this->returnValue(0)); - $ioMock = $this->getMock('Composer\IO\IOInterface'); + $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $ioMock->expects($this->at(0)) ->method('writeError') ->with($this->stringContains('Downgrading')); @@ -649,7 +649,7 @@ composer https://github.com/old/url (push) public function testNotUsingDowngradingWithReferences() { - $oldPackage = $this->getMock('Composer\Package\PackageInterface'); + $oldPackage = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $oldPackage->expects($this->any()) ->method('getVersion') ->will($this->returnValue('dev-ref')); @@ -660,7 +660,7 @@ composer https://github.com/old/url (push) ->method('getSourceUrls') ->will($this->returnValue(array('/foo/bar', 'https://github.com/composer/composer'))); - $newPackage = $this->getMock('Composer\Package\PackageInterface'); + $newPackage = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $newPackage->expects($this->any()) ->method('getSourceReference') ->will($this->returnValue('ref')); @@ -671,12 +671,12 @@ composer https://github.com/old/url (push) ->method('getVersion') ->will($this->returnValue('dev-ref2')); - $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor->expects($this->any()) ->method('execute') ->will($this->returnValue(0)); - $ioMock = $this->getMock('Composer\IO\IOInterface'); + $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $ioMock->expects($this->at(0)) ->method('writeError') ->with($this->stringContains('updating')); From 936933fa66e70795e8d4ddb0372e1720fdd9c90a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 14:12:01 +0100 Subject: [PATCH 070/321] Fix cache tests --- tests/Composer/Test/CacheTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Composer/Test/CacheTest.php b/tests/Composer/Test/CacheTest.php index a8d3eae4a..1c4c67c98 100644 --- a/tests/Composer/Test/CacheTest.php +++ b/tests/Composer/Test/CacheTest.php @@ -13,6 +13,7 @@ namespace Composer\Test; use Composer\Test\TestCase; +use Composer\Cache; use Composer\Util\Filesystem; class CacheTest extends TestCase @@ -40,7 +41,7 @@ class CacheTest extends TestCase $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $this->cache = $this->getMockBuilder('Composer\Cache') ->setMethods(array('getFinder')) - ->setConstructorArgs(array($io, $this->root, 'a-z0-9.', $this->filesystem)) + ->setConstructorArgs(array($io, $this->root)) ->getMock(); $this->cache ->expects($this->any()) @@ -109,13 +110,12 @@ class CacheTest extends TestCase { $this->filesystem ->method('removeDirectory') - ->with($this->root) + ->with($this->root.'/') ->willReturn(true); - $this->assertTrue($this->cache->clear()); + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->cache = new Cache($io, $this->root, 'a-z0-9.', $this->filesystem); - for ($i = 0; $i < 3; $i++) { - $this->assertFileNotExists("{$this->root}/cached.file{$i}.zip"); - } + $this->assertTrue($this->cache->clear()); } } From d381b3a781b70e92152e752df3c704e700e6270e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 16:59:09 +0100 Subject: [PATCH 071/321] Fix variable name --- src/Composer/Repository/ComposerRepository.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index bb613497f..d75d02bac 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -313,9 +313,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } } } + + // add aliases of matched packages even if they did not match the constraint foreach ($candidates as $candidate) { if ($candidate instanceof AliasPackage) { - if (isset($result[spl_object_hash($candidate->getAliasOf())])) { + if (isset($matches[spl_object_hash($candidate->getAliasOf())])) { $matches[spl_object_hash($candidate)] = $candidate; } } From 169fb2347ac63cf3a105bd65589fc982577d07e2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 17:00:57 +0100 Subject: [PATCH 072/321] Avoid issues with git signatures when running tests --- .../Test/Package/Archiver/ArchivableFilesFinderTest.php | 1 + tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php index f6afe10f1..cea4088b1 100644 --- a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php @@ -189,6 +189,7 @@ class ArchivableFilesFinderTest extends TestCase 'git init && '. 'git config user.email "you@example.com" && '. 'git config user.name "Your Name" && '. + 'git config commit.gpgsign false && '. 'git add .git* && '. 'git commit -m "ignore rules" && '. 'git add . && '. diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index 714c9b923..45a635437 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -125,6 +125,12 @@ class ArchiveManagerTest extends ArchiverTest throw new \RuntimeException('Could not config: '.$this->process->getErrorOutput()); } + $result = $this->process->execute('git config commit.gpgsign false', $output, $this->testDir); + if ($result > 0) { + chdir($currentWorkDir); + throw new \RuntimeException('Could not config: '.$this->process->getErrorOutput()); + } + $result = $this->process->execute('git config user.name "Your Name"', $output, $this->testDir); if ($result > 0) { chdir($currentWorkDir); From 1b7e957cc1b62ad815dfdd4fab912bd8916e428f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 18:12:38 +0100 Subject: [PATCH 073/321] Add EventDispatcher::removeListener to allow deregistration of listeners --- .../EventDispatcher/EventDispatcher.php | 21 +++++++- .../EventDispatcher/EventDispatcherTest.php | 48 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index c37d7cf45..c24660659 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -46,7 +46,7 @@ class EventDispatcher protected $io; protected $loader; protected $process; - protected $listeners; + protected $listeners = array(); private $eventStack; /** @@ -172,6 +172,9 @@ class EventDispatcher throw new \RuntimeException('Subscriber '.$className.'::'.$callable[1].' for event '.$event->getName().' is not callable, make sure the function is defined and public'); } + if (is_array($callable) && (is_string($callable[0]) || is_object($callable[0])) && is_string($callable[1])) { + $this->io->writeError(sprintf('> %s: %s', $event->getName(), (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]).'->'.$callable[1] ), true, IOInterface::VERBOSE); + } $event = $this->checkListenerExpectedEvent($callable, $event); $return = false === call_user_func($callable, $event) ? 1 : 0; } elseif ($this->isComposerScript($callable)) { @@ -364,6 +367,22 @@ class EventDispatcher $this->listeners[$eventName][$priority][] = $listener; } + /** + * @param callable|object $listener A callable or an object instance for which all listeners should be removed + */ + public function removeListener($listener) + { + foreach ($this->listeners as $eventName => $priorities) { + foreach ($priorities as $priority => $listeners) { + foreach ($listeners as $index => $candidate) { + if ($listener === $candidate || (is_array($candidate) && is_object($listener) && $candidate[0] === $listener)) { + unset($this->listeners[$eventName][$priority][$index]); + } + } + } + } + } + /** * Adds object methods as listeners for the events in getSubscribedEvents * diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 6d812e20a..a41d745ff 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -171,6 +171,49 @@ class EventDispatcherTest extends TestCase return $rm; } + public function testDispatcherRemoveListener() + { + $composer = $this->createComposerInstance(); + + $composer->setRepositoryManager($this->getRepositoryManagerMockForDevModePassingTest()); + $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock()); + + $dispatcher = new EventDispatcher( + $composer, + $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE), + $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock() + ); + + $listener = array($this, 'someMethod'); + $listener2 = array($this, 'someMethod2'); + $listener3 = 'Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod'; + + $dispatcher->addListener('ev1', $listener, 0); + $dispatcher->addListener('ev1', $listener, 1); + $dispatcher->addListener('ev1', $listener2, 1); + $dispatcher->addListener('ev1', $listener3); + $dispatcher->addListener('ev2', $listener3); + $dispatcher->addListener('ev2', $listener); + $dispatcher->dispatch('ev1'); + $dispatcher->dispatch('ev2'); + + $expected = '> ev1: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod'.PHP_EOL + .'> ev1: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod2'.PHP_EOL + .'> ev1: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod'.PHP_EOL + .'> ev1: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL + .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL + .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod'.PHP_EOL; + $this->assertEquals($expected, $io->getOutput()); + + $dispatcher->removeListener($this); + $dispatcher->dispatch('ev1'); + $dispatcher->dispatch('ev2'); + + $expected .= '> ev1: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL + .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL; + $this->assertEquals($expected, $io->getOutput()); + } + public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack() { $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); @@ -446,6 +489,11 @@ class EventDispatcherTest extends TestCase return true; } + public static function someMethod2() + { + return true; + } + private function createComposerInstance() { $composer = new Composer; From 3fc9ede24b85cbca0914060e6043abdc2cb1e406 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Feb 2019 18:14:46 +0100 Subject: [PATCH 074/321] Add plugin callbacks for deactivation and uninstall, fixes #3000 --- src/Composer/Installer/PluginInstaller.php | 24 ++-- src/Composer/Plugin/PluginInterface.php | 18 +++ src/Composer/Plugin/PluginManager.php | 108 +++++++++++++++++- .../Fixtures/plugin-v1/Installer/Plugin.php | 11 ++ .../Fixtures/plugin-v2/Installer/Plugin2.php | 11 ++ .../Fixtures/plugin-v3/Installer/Plugin2.php | 11 ++ .../Fixtures/plugin-v4/Installer/Plugin1.php | 11 ++ .../Fixtures/plugin-v4/Installer/Plugin2.php | 11 ++ .../Fixtures/plugin-v5/Installer/Plugin5.php | 11 ++ .../Fixtures/plugin-v6/Installer/Plugin6.php | 11 ++ .../Fixtures/plugin-v7/Installer/Plugin7.php | 11 ++ .../Fixtures/plugin-v8/Installer/Plugin8.php | 11 ++ .../Fixtures/plugin-v9/Installer/Plugin.php | 11 ++ .../Test/Plugin/PluginInstallerTest.php | 32 +++++- 14 files changed, 282 insertions(+), 10 deletions(-) diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index 62a16fc62..a52e1937e 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -70,7 +70,7 @@ class PluginInstaller extends LibraryInstaller $this->composer->getPluginManager()->registerPackage($package, true); } catch (\Exception $e) { // Rollback installation - $this->io->writeError('Plugin installation failed, rolling back'); + $this->io->writeError('Plugin initialization failed, uninstalling plugin'); parent::uninstall($repo, $package); throw $e; } @@ -81,12 +81,22 @@ class PluginInstaller extends LibraryInstaller */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { - $extra = $target->getExtra(); - if (empty($extra['class'])) { - throw new \UnexpectedValueException('Error while installing '.$target->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); - } - parent::update($repo, $initial, $target); - $this->composer->getPluginManager()->registerPackage($target, true); + + try { + $this->composer->getPluginManager()->deactivatePackage($initial, true); + $this->composer->getPluginManager()->registerPackage($target, true); + } catch (\Exception $e) { + // Rollback installation + $this->io->writeError('Plugin initialization failed, uninstalling plugin'); + parent::uninstall($repo, $target); + throw $e; + } + } + + public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) + { + $this->composer->getPluginManager()->uninstallPackage($package, true); + parent::uninstall($repo, $package); } } diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index 5158b66f6..27b8c9754 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -36,4 +36,22 @@ interface PluginInterface * @param IOInterface $io */ public function activate(Composer $composer, IOInterface $io); + + /** + * Remove any hooks from Composer + * + * @param Composer $composer + * @param IOInterface $io + */ + public function deactivate(Composer $composer, IOInterface $io); + + /** + * Prepare the plugin to be uninstalled + * + * This will be called after deactivate + * + * @param Composer $composer + * @param IOInterface $io + */ + public function uninstall(Composer $composer, IOInterface $io); } diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 786d846c5..7408359bd 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -144,7 +144,7 @@ class PluginManager $oldInstallerPlugin = ($package->getType() === 'composer-installer'); - if (in_array($package->getName(), $this->registeredPlugins)) { + if (isset($this->registeredPlugins[$package->getName()])) { return; } @@ -200,16 +200,82 @@ class PluginManager if ($oldInstallerPlugin) { $installer = new $class($this->io, $this->composer); $this->composer->getInstallationManager()->addInstaller($installer); + $this->registeredPlugins[$package->getName()] = $installer; } elseif (class_exists($class)) { $plugin = new $class(); $this->addPlugin($plugin); - $this->registeredPlugins[] = $package->getName(); + $this->registeredPlugins[$package->getName()] = $plugin; } elseif ($failOnMissingClasses) { throw new \UnexpectedValueException('Plugin '.$package->getName().' could not be initialized, class not found: '.$class); } } } + /** + * Deactivates a plugin package + * + * If it's of type composer-installer it is unregistered from the installers + * instead for BC + * + * @param PackageInterface $package + * + * @throws \UnexpectedValueException + */ + public function deactivatePackage(PackageInterface $package) + { + if ($this->disablePlugins) { + return; + } + + $oldInstallerPlugin = ($package->getType() === 'composer-installer'); + + if (!isset($this->registeredPlugins[$package->getName()])) { + return; + } + + if ($oldInstallerPlugin) { + $installer = $this->registeredPlugins[$package->getName()]; + unset($this->registeredPlugins[$package->getName()]); + $this->composer->getInstallationManager()->removeInstaller($installer); + } else { + $plugin = $this->registeredPlugins[$package->getName()]; + unset($this->registeredPlugins[$package->getName()]); + $this->removePlugin($plugin); + } + } + + /** + * Uninstall a plugin package + * + * If it's of type composer-installer it is unregistered from the installers + * instead for BC + * + * @param PackageInterface $package + * + * @throws \UnexpectedValueException + */ + public function uninstallPackage(PackageInterface $package) + { + if ($this->disablePlugins) { + return; + } + + $oldInstallerPlugin = ($package->getType() === 'composer-installer'); + + if (!isset($this->registeredPlugins[$package->getName()])) { + return; + } + + if ($oldInstallerPlugin) { + $this->deactivatePackage($package); + } else { + $plugin = $this->registeredPlugins[$package->getName()]; + unset($this->registeredPlugins[$package->getName()]); + $this->removePlugin($plugin); + $this->uninstallPlugin($plugin); + } + } + /** * Returns the version of the internal composer-plugin-api package. * @@ -240,6 +306,44 @@ class PluginManager } } + /** + * Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance + * + * Ideally plugin packages should be deactivated via deactivatePackage, but if you use Composer + * programmatically and want to deregister a plugin class directly this is a valid way + * to do it. + * + * @param PluginInterface $plugin plugin instance + */ + public function removePlugin(PluginInterface $plugin) + { + $index = array_search($plugin, $this->plugins, true); + if ($index === false) { + return; + } + + $this->io->writeError('Unloading plugin '.get_class($plugin), true, IOInterface::DEBUG); + unset($this->plugins[$index]); + $plugin->deactivate($this->composer, $this->io); + + $this->composer->getEventDispatcher()->removeListener($plugin); + } + + /** + * Notifies a plugin it is being uninstalled and should clean up + * + * Ideally plugin packages should be uninstalled via uninstallPackage, but if you use Composer + * programmatically and want to deregister a plugin class directly this is a valid way + * to do it. + * + * @param PluginInterface $plugin plugin instance + */ + public function uninstallPlugin(PluginInterface $plugin) + { + $this->io->writeError('Uninstalling plugin '.get_class($plugin), true, IOInterface::DEBUG); + $plugin->uninstall($this->composer, $this->io); + } + /** * Load all plugins and installers from a repository * diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php index f80acd325..c757d4b09 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php @@ -12,5 +12,16 @@ class Plugin implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v1'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v1'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v1'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php index db5a4462e..32090b66d 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php @@ -12,5 +12,16 @@ class Plugin2 implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v2'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v2'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v2'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php index 861c1679b..034388162 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php @@ -12,5 +12,16 @@ class Plugin2 implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v3'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v3'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v3'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php index 93bcabc98..2eaee6a3f 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php @@ -13,5 +13,16 @@ class Plugin1 implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v4-plugin1'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v4-plugin1'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v4-plugin1'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php index d946deb89..3c5311a82 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php @@ -13,5 +13,16 @@ class Plugin2 implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v4-plugin2'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v4-plugin2'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v4-plugin2'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php index a2ac37bc5..fb9f08a6d 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php @@ -10,5 +10,16 @@ class Plugin5 implements PluginInterface { public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v5'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v5'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v5'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php index e46c0fcb0..acce1f972 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php @@ -10,5 +10,16 @@ class Plugin6 implements PluginInterface { public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v6'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v6'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v6'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php index 5560a6047..84734ce3b 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php @@ -10,5 +10,16 @@ class Plugin7 implements PluginInterface { public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v7'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v7'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v7'); } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php index 7e9a0aab1..4534e13ef 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v8/Installer/Plugin8.php @@ -13,6 +13,17 @@ class Plugin8 implements PluginInterface, Capable public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v8'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v8'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v8'); } public function getCapabilities() diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php index 74e1beb8b..870f11cd1 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v9/Installer/Plugin.php @@ -14,5 +14,16 @@ class Plugin implements PluginInterface public function activate(Composer $composer, IOInterface $io) { + $io->write('activate v9'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $io->write('deactivate v9'); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + $io->write('uninstall v9'); } } diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index 633c5ab18..bd83ce16f 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -19,6 +19,9 @@ use Composer\Package\CompletePackage; use Composer\Package\Loader\JsonLoader; use Composer\Package\Loader\ArrayLoader; use Composer\Plugin\PluginManager; +use Symfony\Component\Console\Output\OutputInterface; +use Composer\IO\BufferIO; +use Composer\EventDispatcher\EventDispatcher; use Composer\Autoload\AutoloadGenerator; use Composer\Test\TestCase; use Composer\Util\Filesystem; @@ -96,7 +99,7 @@ class PluginInstallerTest extends TestCase return __DIR__.'/Fixtures/'.$package->getPrettyName(); })); - $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->io = new BufferIO(); $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); $this->autoloadGenerator = new AutoloadGenerator($dispatcher); @@ -108,6 +111,7 @@ class PluginInstallerTest extends TestCase $this->composer->setRepositoryManager($rm); $this->composer->setInstallationManager($im); $this->composer->setAutoloadGenerator($this->autoloadGenerator); + $this->composer->setEventDispatcher(new EventDispatcher($this->composer, $this->io)); $this->pm = new PluginManager($this->io, $this->composer); $this->composer->setPluginManager($this->pm); @@ -140,6 +144,7 @@ class PluginInstallerTest extends TestCase $plugins = $this->pm->getPlugins(); $this->assertEquals('installer-v1', $plugins[0]->version); + $this->assertEquals('activate v1'.PHP_EOL, $this->io->getOutput()); } public function testInstallMultiplePlugins() @@ -158,6 +163,7 @@ class PluginInstallerTest extends TestCase $this->assertEquals('installer-v4', $plugins[0]->version); $this->assertEquals('plugin2', $plugins[1]->name); $this->assertEquals('installer-v4', $plugins[1]->version); + $this->assertEquals('activate v4-plugin1'.PHP_EOL.'activate v4-plugin2'.PHP_EOL, $this->io->getOutput()); } public function testUpgradeWithNewClassName() @@ -176,7 +182,29 @@ class PluginInstallerTest extends TestCase $installer->update($this->repository, $this->packages[0], $this->packages[1]); $plugins = $this->pm->getPlugins(); + $this->assertCount(1, $plugins); $this->assertEquals('installer-v2', $plugins[1]->version); + $this->assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'activate v2'.PHP_EOL, $this->io->getOutput()); + } + + public function testUninstall() + { + $this->repository + ->expects($this->once()) + ->method('getPackages') + ->will($this->returnValue(array($this->packages[0]))); + $this->repository + ->expects($this->exactly(1)) + ->method('hasPackage') + ->will($this->onConsecutiveCalls(true, false)); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); + + $installer->uninstall($this->repository, $this->packages[0]); + + $plugins = $this->pm->getPlugins(); + $this->assertCount(0, $plugins); + $this->assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'uninstall v1'.PHP_EOL, $this->io->getOutput()); } public function testUpgradeWithSameClassName() @@ -196,6 +224,7 @@ class PluginInstallerTest extends TestCase $plugins = $this->pm->getPlugins(); $this->assertEquals('installer-v3', $plugins[1]->version); + $this->assertEquals('activate v2'.PHP_EOL.'deactivate v2'.PHP_EOL.'activate v3'.PHP_EOL, $this->io->getOutput()); } public function testRegisterPluginOnlyOneTime() @@ -213,6 +242,7 @@ class PluginInstallerTest extends TestCase $plugins = $this->pm->getPlugins(); $this->assertCount(1, $plugins); $this->assertEquals('installer-v1', $plugins[0]->version); + $this->assertEquals('activate v1'.PHP_EOL, $this->io->getOutput()); } /** From 6c782599f1e2e4a07a102aa28d5f70d39300bf95 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 19 Feb 2019 10:54:42 +0100 Subject: [PATCH 075/321] Make IOInterface implement psr-4 LoggerInterface, fixes #5180 --- src/Composer/IO/BaseIO.php | 3 +-- src/Composer/IO/IOInterface.php | 3 ++- src/Composer/Util/Http/CurlDownloader.php | 1 - src/Composer/Util/HttpDownloader.php | 4 +--- src/Composer/Util/RemoteFilesystem.php | 4 +--- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index b327f1bbf..b63b59484 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -14,10 +14,9 @@ namespace Composer\IO; use Composer\Config; use Composer\Util\ProcessExecutor; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; -abstract class BaseIO implements IOInterface, LoggerInterface +abstract class BaseIO implements IOInterface { protected $authentications = array(); diff --git a/src/Composer/IO/IOInterface.php b/src/Composer/IO/IOInterface.php index 5766ba479..46302088f 100644 --- a/src/Composer/IO/IOInterface.php +++ b/src/Composer/IO/IOInterface.php @@ -13,13 +13,14 @@ namespace Composer\IO; use Composer\Config; +use Psr\Log\LoggerInterface; /** * The Input/Output helper interface. * * @author François Pluchino */ -interface IOInterface +interface IOInterface extends LoggerInterface { const QUIET = 1; const NORMAL = 2; diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index ff31bf695..ab0dae91e 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -20,7 +20,6 @@ use Composer\Util\RemoteFilesystem; use Composer\Util\StreamContextFactory; use Composer\Util\AuthHelper; use Composer\Util\Url; -use Psr\Log\LoggerInterface; use React\Promise\Promise; /** diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 172ea875a..68e11a4a4 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -17,7 +17,6 @@ use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; use Composer\Util\Http\Response; -use Psr\Log\LoggerInterface; use React\Promise\Promise; /** @@ -58,8 +57,7 @@ class HttpDownloader // Setup TLS options // The cafile option can be set via config.json if ($disableTls === false) { - $logger = $io instanceof LoggerInterface ? $io : null; - $this->options = StreamContextFactory::getTlsDefaults($options, $logger); + $this->options = StreamContextFactory::getTlsDefaults($options, $io); } else { $this->disableTls = true; } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 2709f7006..e2c50472c 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -16,7 +16,6 @@ use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; -use Psr\Log\LoggerInterface; /** * @author François Pluchino @@ -61,8 +60,7 @@ class RemoteFilesystem // Setup TLS options // The cafile option can be set via config.json if ($disableTls === false) { - $logger = $io instanceof LoggerInterface ? $io : null; - $this->options = StreamContextFactory::getTlsDefaults($options, $logger); + $this->options = StreamContextFactory::getTlsDefaults($options, $io); } else { $this->disableTls = true; } From ab945a6ec1899d5f5f5f844d39d5e921ddcebccd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 19 Feb 2019 11:11:35 +0100 Subject: [PATCH 076/321] Clean up RepositoryInterface, fixes #5464 --- src/Composer/Repository/RepositoryInterface.php | 3 ++- src/Composer/Repository/RepositoryManager.php | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 567455163..e5f2c5159 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -78,8 +78,9 @@ interface RepositoryInterface extends \Countable * * @param string $query search query * @param int $mode a set of SEARCH_* constants to search on, implementations should do a best effort only + * @param string $type The type of package to search for. Defaults to all types of packages * * @return array[] an array of array('name' => '...', 'description' => '...') */ - public function search($query, $mode = 0); + public function search($query, $mode = 0, $type = null); } diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index c3ce0c24a..2dca57099 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -125,13 +125,7 @@ class RepositoryManager $class = $this->repositoryClasses[$type]; - $reflMethod = new \ReflectionMethod($class, '__construct'); - $params = $reflMethod->getParameters(); - if (isset($params[3]) && $params[3]->getClass() && $params[3]->getClass()->getName() === 'Composer\Util\HttpDownloader') { - return new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher); - } - - return new $class($config, $this->io, $this->config, $this->eventDispatcher); + return new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher); } /** From 65903aacfdc19acb52c441dc5567d45052cfa169 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Tue, 19 Feb 2019 09:35:48 -0500 Subject: [PATCH 077/321] Fix type issues (#7996) * Fix type issues found by Psalm --- src/Composer/Command/BaseCommand.php | 6 ++++-- src/Composer/Command/ConfigCommand.php | 2 +- src/Composer/Command/DiagnoseCommand.php | 16 +--------------- src/Composer/Command/ShowCommand.php | 12 ++++++------ src/Composer/Command/SuggestsCommand.php | 5 ++++- src/Composer/Config/ConfigSourceInterface.php | 2 +- src/Composer/Config/JsonConfigSource.php | 2 +- src/Composer/Console/Application.php | 2 +- src/Composer/DependencyResolver/GenericRule.php | 8 ++++---- src/Composer/DependencyResolver/PoolBuilder.php | 6 +++--- src/Composer/DependencyResolver/Problem.php | 2 +- src/Composer/DependencyResolver/Solver.php | 5 +++-- src/Composer/Downloader/DownloadManager.php | 4 +--- src/Composer/Downloader/GitDownloader.php | 6 +++--- src/Composer/Downloader/PerforceDownloader.php | 2 +- src/Composer/EventDispatcher/EventDispatcher.php | 3 ++- src/Composer/Factory.php | 2 +- src/Composer/IO/IOInterface.php | 6 +++--- src/Composer/Installer.php | 2 +- src/Composer/Installer/InstallerInterface.php | 2 +- src/Composer/Installer/LibraryInstaller.php | 2 +- src/Composer/Package/BasePackage.php | 2 +- src/Composer/Package/PackageInterface.php | 2 +- src/Composer/Package/Version/VersionSelector.php | 2 +- .../Repository/Pear/BaseChannelReader.php | 2 +- .../Repository/Pear/ChannelRest10Reader.php | 2 +- src/Composer/Repository/PearRepository.php | 2 +- .../Repository/Vcs/VcsDriverInterface.php | 6 +++--- src/Composer/Util/AuthHelper.php | 2 +- src/Composer/Util/Filesystem.php | 1 + src/Composer/Util/HttpDownloader.php | 2 +- src/Composer/Util/Loop.php | 1 + src/Composer/Util/RemoteFilesystem.php | 2 +- src/Composer/Util/Svn.php | 2 +- 34 files changed, 59 insertions(+), 66 deletions(-) diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php index 888b2a7f2..56ee9f7f4 100644 --- a/src/Composer/Command/BaseCommand.php +++ b/src/Composer/Command/BaseCommand.php @@ -27,6 +27,8 @@ use Symfony\Component\Console\Command\Command; /** * Base class for Composer commands * + * @method Application getApplication() + * * @author Ryan Weaver * @author Konstantin Kudryashov */ @@ -46,7 +48,7 @@ abstract class BaseCommand extends Command * @param bool $required * @param bool|null $disablePlugins * @throws \RuntimeException - * @return Composer + * @return Composer|null */ public function getComposer($required = true, $disablePlugins = null) { @@ -173,7 +175,7 @@ abstract class BaseCommand extends Command if ($input->getOption('prefer-source') || $input->getOption('prefer-dist') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs'))) { $preferSource = $input->getOption('prefer-source') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs')); - $preferDist = $input->getOption('prefer-dist'); + $preferDist = (bool) $input->getOption('prefer-dist'); } return array($preferSource, $preferDist); diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 89ba495cf..3970d915d 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -226,7 +226,7 @@ EOT } $settingKey = $input->getArgument('setting-key'); - if (!$settingKey) { + if (!$settingKey || !is_string($settingKey)) { return 0; } diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 481d58060..c123e7003 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -602,20 +602,6 @@ EOT $text .= "Install either of them or recompile php without --disable-iconv"; break; - case 'unicode': - $text = PHP_EOL."The detect_unicode setting must be disabled.".PHP_EOL; - $text .= "Add the following to the end of your `php.ini`:".PHP_EOL; - $text .= " detect_unicode = Off"; - $displayIniMessage = true; - break; - - case 'suhosin': - $text = PHP_EOL."The suhosin.executor.include.whitelist setting is incorrect.".PHP_EOL; - $text .= "Add the following to the end of your `php.ini` or suhosin.ini (Example path [for Debian]: /etc/php5/cli/conf.d/suhosin.ini):".PHP_EOL; - $text .= " suhosin.executor.include.whitelist = phar ".$current; - $displayIniMessage = true; - break; - case 'php': $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher."; break; @@ -713,7 +699,7 @@ EOT /** * Check if allow_url_fopen is ON * - * @return bool|string + * @return true|string */ private function checkConnectivity() { diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 5cb3fa860..a5a2cd780 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -823,10 +823,10 @@ EOT /** * Display a package tree * - * @param PackageInterface|string $package - * @param array $packagesInTree - * @param string $previousTreeBar - * @param int $level + * @param array|string $package + * @param array $packagesInTree + * @param string $previousTreeBar + * @param int $level */ protected function displayTree( $package, @@ -835,7 +835,7 @@ EOT $level = 1 ) { $previousTreeBar = str_replace('├', '│', $previousTreeBar); - if (isset($package['requires'])) { + if (is_array($package) && isset($package['requires'])) { $requires = $package['requires']; $treeBar = $previousTreeBar . ' ├'; $i = 0; @@ -968,7 +968,7 @@ EOT * @param string $phpVersion * @param bool $minorOnly * - * @return PackageInterface|null + * @return PackageInterface|false */ private function findLatestPackage(PackageInterface $package, Composer $composer, $phpVersion, $minorOnly = false) { diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 225725e12..1c1f23f93 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -43,6 +43,9 @@ EOT ; } + /** + * {@inheritDoc} + */ protected function execute(InputInterface $input, OutputInterface $output) { $lock = $this->getComposer()->getLocker()->getLockData(); @@ -117,7 +120,7 @@ EOT $io->write(sprintf('%s', $suggestion)); } - return; + return null; } // Grouped by package diff --git a/src/Composer/Config/ConfigSourceInterface.php b/src/Composer/Config/ConfigSourceInterface.php index 0d56fc0ed..a00e989bb 100644 --- a/src/Composer/Config/ConfigSourceInterface.php +++ b/src/Composer/Config/ConfigSourceInterface.php @@ -39,7 +39,7 @@ interface ConfigSourceInterface * Add a config setting * * @param string $name Name - * @param string $value Value + * @param string|array $value Value */ public function addConfigSetting($name, $value); diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index 15d40d200..e22bbb1e5 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -259,7 +259,7 @@ class JsonConfigSource implements ConfigSourceInterface * * @param array $array * @param mixed $value - * @return array + * @return int */ private function arrayUnshiftRef(&$array, &$value) { diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 0bfee80f3..64a32b46f 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -284,7 +284,7 @@ class Application extends BaseApplication return $result; } catch (ScriptExecutionException $e) { - return $e->getCode(); + return (int) $e->getCode(); } catch (\Exception $e) { $this->hintCommonErrors($e); restore_error_handler(); diff --git a/src/Composer/DependencyResolver/GenericRule.php b/src/Composer/DependencyResolver/GenericRule.php index df8a2a003..eb753067e 100644 --- a/src/Composer/DependencyResolver/GenericRule.php +++ b/src/Composer/DependencyResolver/GenericRule.php @@ -23,10 +23,10 @@ class GenericRule extends Rule protected $literals; /** - * @param array $literals - * @param int $reason A RULE_* constant describing the reason for generating this rule - * @param Link|PackageInterface $reasonData - * @param array $job The job this rule was created from + * @param array $literals + * @param int|null $reason A RULE_* constant describing the reason for generating this rule + * @param Link|PackageInterface|int|null $reasonData + * @param array $job The job this rule was created from */ public function __construct(array $literals, $reason, $reasonData, $job = null) { diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 9eb3dc0d0..42444cc91 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -176,12 +176,12 @@ class PoolBuilder if (!isset($this->loadedNames[$require])) { $loadNames[$require] = null; } - if ($link->getConstraint()) { + if ($linkConstraint = $link->getConstraint()) { if (!array_key_exists($require, $this->nameConstraints)) { - $this->nameConstraints[$require] = new MultiConstraint(array($link->getConstraint()), false); + $this->nameConstraints[$require] = new MultiConstraint(array($linkConstraint), false); } elseif ($this->nameConstraints[$require]) { // TODO addConstraint function? - $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($link->getConstraint()), $this->nameConstraints[$require]->getConstraints()), false); + $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($linkConstraint), $this->nameConstraints[$require]->getConstraints()), false); } } else { $this->nameConstraints[$require] = null; diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 271c7261f..98b07405a 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -180,7 +180,7 @@ class Problem * Store a reason descriptor but ignore duplicates * * @param string $id A canonical identifier for the reason - * @param string $reason The reason descriptor + * @param string|array $reason The reason descriptor */ protected function addReason($id, $reason) { diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 2188d99ce..821928382 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -13,6 +13,7 @@ namespace Composer\DependencyResolver; use Composer\IO\IOInterface; +use Composer\Package\PackageInterface; use Composer\Repository\RepositoryInterface; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositorySet; @@ -44,7 +45,7 @@ class Solver protected $watchGraph; /** @var Decisions */ protected $decisions; - /** @var int[] */ + /** @var PackageInterface[] */ protected $installedMap; /** @var int */ @@ -691,7 +692,7 @@ class Solver /** * @todo this makes $disableRules always false; determine the rationale and possibly remove dead code? */ - $disableRules = array(); + $disableRules = false; $level = 1; $systemLevel = $level + 1; diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index 0b1ddb5a6..a23c167b5 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -294,9 +294,7 @@ class DownloadManager // if downloader type changed, or update failed and user asks for reinstall, // we wipe the dir and do a new install instead of updating it - if ($initialDownloader) { - $initialDownloader->remove($initial, $targetDir); - } + $initialDownloader->remove($initial, $targetDir); $this->install($target, $targetDir); } diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 96e47cb22..f698981fe 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -362,7 +362,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface ) { $command = sprintf('git checkout '.$force.'-B %s %s -- && git reset --hard %2$s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$reference)); if (0 === $this->process->execute($command, $output, $path)) { - return; + return null; } } @@ -380,14 +380,14 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface ) { $command = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference)); if (0 === $this->process->execute($command, $output, $path)) { - return; + return null; } } } $command = sprintf($template, ProcessExecutor::escape($gitRef)); if (0 === $this->process->execute($command, $output, $path)) { - return; + return null; } // reference was not found (prints "fatal: reference is not a tree: $ref") diff --git a/src/Composer/Downloader/PerforceDownloader.php b/src/Composer/Downloader/PerforceDownloader.php index a7dc013b3..0427ec8c8 100644 --- a/src/Composer/Downloader/PerforceDownloader.php +++ b/src/Composer/Downloader/PerforceDownloader.php @@ -88,7 +88,7 @@ class PerforceDownloader extends VcsDownloader { $this->io->writeError('Perforce driver does not check for local changes before overriding', true); - return; + return null; } /** diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index c24660659..05e34dda1 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -199,6 +199,7 @@ class EventDispatcher } try { + /** @var InstallerEvent $event */ $return = $this->dispatch($scriptName, new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags)); } catch (ScriptExecutionException $e) { $this->io->writeError(sprintf('Script %s was called via %s', $callable, $event->getName()), true, IOInterface::QUIET); @@ -499,7 +500,7 @@ class EventDispatcher * * @param Event $event * @throws \RuntimeException - * @return number + * @return int */ protected function pushEvent(Event $event) { diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index b2fd4efab..383337aa6 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -413,7 +413,7 @@ class Factory /** * @param IOInterface $io IO instance * @param bool $disablePlugins Whether plugins should not be loaded - * @return Composer + * @return Composer|null */ public static function createGlobal(IOInterface $io, $disablePlugins = false) { diff --git a/src/Composer/IO/IOInterface.php b/src/Composer/IO/IOInterface.php index 46302088f..95f891c57 100644 --- a/src/Composer/IO/IOInterface.php +++ b/src/Composer/IO/IOInterface.php @@ -108,7 +108,7 @@ interface IOInterface extends LoggerInterface * @param string $default The default answer if none is given by the user * * @throws \RuntimeException If there is no data to read in the input stream - * @return string The user answer + * @return string|null The user answer */ public function ask($question, $default = null); @@ -146,7 +146,7 @@ interface IOInterface extends LoggerInterface * * @param string $question The question to ask * - * @return string The answer + * @return string|null The answer */ public function askAndHideAnswer($question); @@ -161,7 +161,7 @@ interface IOInterface extends LoggerInterface * @param bool $multiselect Select more than one value separated by comma * * @throws \InvalidArgumentException - * @return int|string|array The selected value or values (the key of the choices array) + * @return int|string|array|bool The selected value or values (the key of the choices array) */ public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 19c0015d6..fc877f18e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -959,7 +959,7 @@ class Installer * @param RepositoryInterface $lockedRepository * @param string $task * @param array|null $operations - * @return array + * @return array|null */ private function processDevPackages($localRepo, Pool $pool, $policy, $repositories, $installedRepo, $lockedRepository, $task, array $operations = null) { diff --git a/src/Composer/Installer/InstallerInterface.php b/src/Composer/Installer/InstallerInterface.php index e00877ed9..310c5fcfc 100644 --- a/src/Composer/Installer/InstallerInterface.php +++ b/src/Composer/Installer/InstallerInterface.php @@ -48,7 +48,7 @@ interface InstallerInterface * * @param PackageInterface $package package instance * @param PackageInterface $prevPackage previous package instance in case of an update - * @return PromiseInterface + * @return PromiseInterface|null */ public function download(PackageInterface $package, PackageInterface $prevPackage = null); diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 4c2f45601..a89553b1b 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -43,7 +43,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface * * @param IOInterface $io * @param Composer $composer - * @param string $type + * @param string|null $type * @param Filesystem $filesystem * @param BinaryInstaller $binaryInstaller */ diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index f2f5be707..9630e7ef0 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -239,7 +239,7 @@ abstract class BasePackage implements PackageInterface * Build a regexp from a package name, expanding * globs as required * * @param string $whiteListedPattern - * @param bool $wrap Wrap the cleaned string by the given string + * @param string $wrap Wrap the cleaned string by the given string * @return string */ public static function packageNameToRegexp($whiteListedPattern, $wrap = '{^%s$}i') diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 73d2ade41..30488e89f 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -76,7 +76,7 @@ interface PackageInterface /** * Returns the package targetDir property * - * @return string The package targetDir + * @return string|null The package targetDir */ public function getTargetDir(); diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index d99780ab1..a8b4ae17b 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -45,7 +45,7 @@ class VersionSelector * @param string $targetPackageVersion * @param string $targetPhpVersion * @param string $preferredStability - * @return PackageInterface|bool + * @return PackageInterface|false */ public function findBestCandidate($packageName, $targetPackageVersion = null, $targetPhpVersion = null, $preferredStability = 'stable') { diff --git a/src/Composer/Repository/Pear/BaseChannelReader.php b/src/Composer/Repository/Pear/BaseChannelReader.php index b778bf08b..9b9acf2f2 100644 --- a/src/Composer/Repository/Pear/BaseChannelReader.php +++ b/src/Composer/Repository/Pear/BaseChannelReader.php @@ -47,7 +47,7 @@ abstract class BaseChannelReader * @param string $origin server * @param string $path relative path to content * @throws \UnexpectedValueException - * @return \SimpleXMLElement + * @return string */ protected function requestContent($origin, $path) { diff --git a/src/Composer/Repository/Pear/ChannelRest10Reader.php b/src/Composer/Repository/Pear/ChannelRest10Reader.php index 93969043a..9d14b71ea 100644 --- a/src/Composer/Repository/Pear/ChannelRest10Reader.php +++ b/src/Composer/Repository/Pear/ChannelRest10Reader.php @@ -150,7 +150,7 @@ class ChannelRest10Reader extends BaseChannelReader * @param string $baseUrl * @param string $packageName * @param string $version - * @return DependencyInfo[] + * @return DependencyInfo */ private function readPackageReleaseDependencies($baseUrl, $packageName, $version) { diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index 1bb22c0ed..5cffb6233 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -97,7 +97,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn * * @param ChannelInfo $channelInfo * @param SemverVersionParser $versionParser - * @return CompletePackage + * @return CompletePackage[] */ private function buildComposerPackages(ChannelInfo $channelInfo, SemverVersionParser $versionParser) { diff --git a/src/Composer/Repository/Vcs/VcsDriverInterface.php b/src/Composer/Repository/Vcs/VcsDriverInterface.php index 5e3bcec68..e59bcf647 100644 --- a/src/Composer/Repository/Vcs/VcsDriverInterface.php +++ b/src/Composer/Repository/Vcs/VcsDriverInterface.php @@ -38,7 +38,7 @@ interface VcsDriverInterface * * @param string $file * @param string $identifier - * @return string + * @return string|null */ public function getFileContent($file, $identifier); @@ -46,7 +46,7 @@ interface VcsDriverInterface * Get the changedate for $identifier. * * @param string $identifier - * @return \DateTime + * @return \DateTime|null */ public function getChangeDate($identifier); @@ -73,7 +73,7 @@ interface VcsDriverInterface /** * @param string $identifier Any identifier to a specific branch/tag/commit - * @return array With type, url reference and shasum keys. + * @return array|null With type, url reference and shasum keys. */ public function getDist($identifier); diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index 3679b93da..2868d3346 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -73,7 +73,7 @@ class AuthHelper * @param string|null $reason a message/description explaining why this was called * @param string $warning an authentication warning returned by the server as {"warning": ".."}, if present * @param string[] $headers - * @return array containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be + * @return array|null containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be * retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json */ public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $warning = null, $headers = array()) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 805eda14b..2d73016c6 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -292,6 +292,7 @@ class Filesystem $this->ensureDirectoryExists($target); $result = true; + /** @var RecursiveDirectoryIterator $ri */ foreach ($ri as $file) { $targetPath = $target . DIRECTORY_SEPARATOR . $ri->getSubPathName(); if ($file->isDir()) { diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 68e11a4a4..5a5ec14b2 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -117,7 +117,7 @@ class HttpDownloader /** * Merges new options * - * @return array $options + * @return void */ public function setOptions(array $options) { diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index c50cf4b02..dfaa2ac53 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -29,6 +29,7 @@ class Loop public function wait(array $promises) { + /** @var \Exception|null */ $uncaught = null; \React\Promise\all($promises)->then( diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index e2c50472c..07eccc791 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -398,7 +398,7 @@ class RemoteFilesystem // fail 4xx and 5xx responses and capture the response if ($statusCode && $statusCode >= 400 && $statusCode <= 599) { if (!$this->retry) { - if ($this->progress && !$this->retry && !$isRedirect) { + if ($this->progress && !$isRedirect) { $this->io->overwriteError("Downloading (failed)", false); } diff --git a/src/Composer/Util/Svn.php b/src/Composer/Util/Svn.php index 58114ac93..be1a81c91 100644 --- a/src/Composer/Util/Svn.php +++ b/src/Composer/Util/Svn.php @@ -304,7 +304,7 @@ class Svn $this->createAuthFromUrl(); } - return $this->hasAuth; + return (bool) $this->hasAuth; } /** From ff82334124633d35f23343db17017e0d5abab1c2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 20 Feb 2019 08:42:49 +0100 Subject: [PATCH 078/321] Load ~dev files as well as main provider files for new v2 protocol, fixes #6415 --- src/Composer/Repository/ComposerRepository.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index d75d02bac..92d39adfd 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -581,6 +581,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito throw new \LogicException('loadAsyncPackages only supports v2 protocol composer repos with a metadata-url'); } + // load ~dev variants as well if present + // TODO ideally there should be a flag set from the repositoryset/poolbuilder to know which packages should have the dev packages loaded + // so we can optimize away some requests entirely + foreach ($packageNames as $name => $constraint) { + $packageNames[$name.'~dev'] = $constraint; + } + foreach ($packageNames as $name => $constraint) { $name = strtolower($name); From 177f21ec5c975df8771517d53192da7aa2b46651 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 20 Feb 2019 10:51:07 +0100 Subject: [PATCH 079/321] Fix loading of dev providers, refs #6415 --- src/Composer/Repository/ComposerRepository.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 92d39adfd..8d0d8c3a4 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -591,8 +591,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($packageNames as $name => $constraint) { $name = strtolower($name); + $realName = preg_replace('{~dev$}', '', $name); // skip platform packages, root package and composer-plugin-api - if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $realName) || '__root__' === $realName || 'composer-plugin-api' === $realName) { continue; } @@ -606,16 +607,16 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified) - ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) { + ->then(function ($response) use (&$packages, $contents, $realName, $constraint, $repo, $isPackageAcceptableCallable) { if (true === $response) { $response = $contents; } - if (!isset($response['packages'][$name])) { + if (!isset($response['packages'][$realName])) { return; } - $versions = $response['packages'][$name]; + $versions = $response['packages'][$realName]; if (isset($response['minified']) && $response['minified'] === 'composer/2.0') { // TODO extract in other method @@ -649,7 +650,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($versions as $version) { if (isset($version['version_normalizeds'])) { foreach ($version['version_normalizeds'] as $index => $normalizedVersion) { - if (!$repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $normalizedVersion)) { + if (!$repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $realName, $normalizedVersion)) { foreach ($uniqKeys as $key) { unset($version[$key.'s'][$index]); } @@ -663,7 +664,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $version['version_normalized'] = $repo->versionParser->normalize($version['version']); } - if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $version['version_normalized'])) { + if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $realName, $version['version_normalized'])) { $versionsToLoad[] = $version; } } From 2e204b016187c1ce4a50021fc127ff19ea5c650e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 20 Feb 2019 11:10:44 +0100 Subject: [PATCH 080/321] Remove support for the first version of the compression algo (#7906) --- src/Composer/Package/Loader/ArrayLoader.php | 31 +++---------------- .../Repository/ComposerRepository.php | 24 +++----------- 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index c5edd0d68..511f6c7ca 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -62,39 +62,16 @@ class ArrayLoader implements LoaderInterface public function loadPackages(array $versions, $class) { - static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); - $packages = array(); $linkCache = array(); foreach ($versions as $version) { - if (isset($version['versions'])) { - $baseVersion = $version; - foreach ($uniqKeys as $key) { - unset($baseVersion[$key.'s']); - } + $package = $this->createObject($version, $class); - foreach ($version['versions'] as $index => $dummy) { - $unpackedVersion = $baseVersion; - foreach ($uniqKeys as $key) { - $unpackedVersion[$key] = $version[$key.'s'][$index]; - } + $this->configureCachedLinks($linkCache, $package, $version); + $package = $this->configureObject($package, $version); - $package = $this->createObject($unpackedVersion, $class); - - $this->configureCachedLinks($linkCache, $package, $unpackedVersion); - $package = $this->configureObject($package, $unpackedVersion); - - $packages[] = $package; - } - } else { - $package = $this->createObject($version, $class); - - $this->configureCachedLinks($linkCache, $package, $version); - $package = $this->configureObject($package, $version); - - $packages[] = $package; - } + $packages[] = $package; } return $packages; diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 8d0d8c3a4..ccb14f622 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -645,28 +645,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito unset($expanded, $expandedVersion, $versionData); } - static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time'); $versionsToLoad = array(); foreach ($versions as $version) { - if (isset($version['version_normalizeds'])) { - foreach ($version['version_normalizeds'] as $index => $normalizedVersion) { - if (!$repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $realName, $normalizedVersion)) { - foreach ($uniqKeys as $key) { - unset($version[$key.'s'][$index]); - } - } - } - if (count($version['version_normalizeds'])) { - $versionsToLoad[] = $version; - } - } else { - if (!isset($version['version_normalized'])) { - $version['version_normalized'] = $repo->versionParser->normalize($version['version']); - } + if (!isset($version['version_normalized'])) { + $version['version_normalized'] = $repo->versionParser->normalize($version['version']); + } - if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $realName, $version['version_normalized'])) { - $versionsToLoad[] = $version; - } + if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $realName, $version['version_normalized'])) { + $versionsToLoad[] = $version; } } From bdf1f7f82b6c7c876278fdcd2c85fa13b318a6e8 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 20 Feb 2019 13:24:44 +0100 Subject: [PATCH 081/321] Fix loading of aliased packages in ComposerRepository when filtering by callback --- src/Composer/Repository/BaseRepository.php | 8 ++- .../Repository/ComposerRepository.php | 55 ++++++++++++------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/BaseRepository.php index d835d55fd..fb10fb678 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/BaseRepository.php @@ -32,9 +32,11 @@ abstract class BaseRepository implements RepositoryInterface $result = array(); foreach ($packages as $package) { - if (array_key_exists($package->getName(), $packageMap) && - (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) && - call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { + if ( + array_key_exists($package->getName(), $packageMap) + && (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) + && call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability()) + ) { $result[spl_object_hash($package)] = $package; if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index ccb14f622..f12c85b85 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -103,7 +103,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->baseUrl = rtrim(preg_replace('{(?:/[^/\\\\]+\.json)?(?:[?#].*)?$}', '', $this->url), '/'); $this->io = $io; - $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); + $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$~'); $this->versionParser = new VersionParser(); $this->loader = new ArrayLoader($this->versionParser); $this->httpDownloader = $httpDownloader; @@ -139,9 +139,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return; } - $packages = $this->loadAsyncPackages(array($name => $constraint), function ($name, $stability) { - return true; - }); + $packages = $this->loadAsyncPackages(array($name => $constraint)); return reset($packages); } @@ -181,9 +179,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return array(); } - return $this->loadAsyncPackages(array($name => $constraint ?: new EmptyConstraint()), function ($name, $stability) { - return true; - }); + return $this->loadAsyncPackages(array($name => $constraint)); } if ($hasProviders) { @@ -241,7 +237,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $packageMap[$name] = new EmptyConstraint(); } - return array_values($this->loadAsyncPackages($packageMap, function ($name, $stability) { return true; })); + return array_values($this->loadAsyncPackages($packageMap)); } throw new \LogicException('Composer repositories that have lazy providers and no available-packages list can not load the complete list of packages, use getProviderNames instead.'); @@ -513,11 +509,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } if (!isset($versionsToLoad[$version['uid']])) { - if ($isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, $normalizedName, VersionParser::parseStability($version['version']))) { - continue; + if (!isset($version['version_normalized'])) { + $version['version_normalized'] = $this->versionParser->normalize($version['version']); } - $versionsToLoad[$version['uid']] = $version; + if ($this->isVersionAcceptable($isPackageAcceptableCallable, null, $normalizedName, $version)) { + $versionsToLoad[$version['uid']] = $version; + } } } } @@ -569,7 +567,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->configurePackageTransportOptions($package); } - private function loadAsyncPackages(array $packageNames, $isPackageAcceptableCallable) + /** + * @param array $packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only packages matching it will be loaded + */ + private function loadAsyncPackages(array $packageNames, $isPackageAcceptableCallable = null) { $this->loadRootServerFile(); @@ -598,7 +599,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $url = str_replace('%package%', $name, $this->lazyProvidersUrl); - $cacheKey = 'provider-'.strtr($name, '/', '$').'.json'; + $cacheKey = 'provider-'.strtr($name, '/', '~').'.json'; $lastModified = null; if ($contents = $this->cache->read($cacheKey)) { @@ -651,7 +652,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $version['version_normalized'] = $repo->versionParser->normalize($version['version']); } - if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $realName, $version['version_normalized'])) { + if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $realName, $version)) { $versionsToLoad[] = $version; } } @@ -659,9 +660,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $loadedPackages = $repo->createPackages($versionsToLoad, 'Composer\Package\CompletePackage'); foreach ($loadedPackages as $package) { $package->setRepository($repo); - $packages[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { + $package->getAliasOf()->setRepository($repo); $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } } @@ -677,19 +679,30 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito /** * TODO v3 should make this private once we can drop PHP 5.3 support * + * @param string $name package name (must be lowercased already) * @private */ - public function isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $versionNormalized) + public function isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $versionData) { - if (!call_user_func($isPackageAcceptableCallable, strtolower($name), VersionParser::parseStability($versionNormalized))) { - return false; + $versions = array($versionData['version_normalized']); + + if ($alias = $this->loader->getBranchAlias($versionData)) { + $versions[] = $alias; } - if ($constraint && !$constraint->matches(new Constraint('==', $versionNormalized))) { - return false; + foreach ($versions as $version) { + if ($isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, $name, VersionParser::parseStability($version))) { + continue; + } + + if ($constraint && !$constraint->matches(new Constraint('==', $version))) { + continue; + } + + return true; } - return true; + return false; } protected function loadRootServerFile() From 8fe2b9ec69b8da2e4a0ca9629f20559c568bff6a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 20 Feb 2019 13:41:43 +0100 Subject: [PATCH 082/321] Fix test --- tests/Composer/Test/Repository/ComposerRepositoryTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 55ca6bf09..c8af9418c 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -99,7 +99,13 @@ class ComposerRepositoryTest extends TestCase public function testWhatProvides() { $repo = $this->getMockBuilder('Composer\Repository\ComposerRepository') - ->disableOriginalConstructor() + ->setConstructorArgs(array( + array('url' => 'https://dummy.test.link'), + new NullIO, + FactoryMock::createConfig(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() + )) ->setMethods(array('fetchFile')) ->getMock(); From 60df8925174dfb385368efbbfd2d19c7f372c2cd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 21 Feb 2019 12:27:02 +0100 Subject: [PATCH 083/321] Store dev mode in installed.json, fixes #3008 --- .travis.yml | 2 +- src/Composer/Installer.php | 4 ++-- src/Composer/Repository/FilesystemRepository.php | 15 ++++++++++----- .../Repository/WritableArrayRepository.php | 2 +- .../Repository/WritableRepositoryInterface.php | 4 +++- .../Mock/InstalledFilesystemRepositoryMock.php | 2 +- .../Test/Repository/FilesystemRepositoryTest.php | 5 +++-- 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index a06922904..c732f9421 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,7 +62,7 @@ script: - ls -d tests/Composer/Test/* | grep -v TestCase.php | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml --colors=always {} || (echo -e "\e[41mFAILED\e[0m {}" && exit 1);' # Run PHPStan - if [[ $PHPSTAN == "1" ]]; then - composer require --dev phpstan/phpstan-shim:^0.11 --ignore-platform-reqs && + bin/composer require --dev phpstan/phpstan-shim:^0.11 --ignore-platform-reqs && vendor/bin/phpstan.phar analyse src tests --configuration=phpstan/config.neon --autoload-file=phpstan/autoload.php; fi diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index fc877f18e..4dddd9505 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -616,7 +616,7 @@ class Installer } if ($this->executeOperations || $this->writeLock) { - $localRepo->write(); + $localRepo->write($this->devMode); } $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); @@ -628,7 +628,7 @@ class Installer if ($this->executeOperations) { // force source/dist urls to be updated for all packages $this->processPackageUrls($pool, $policy, $localRepo, $repositories); - $localRepo->write(); + $localRepo->write($this->devMode); } return array(0, $devPackages); diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index bde55aad3..9dbac5f76 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -49,7 +49,12 @@ class FilesystemRepository extends WritableArrayRepository } try { - $packages = $this->file->read(); + $data = $this->file->read(); + if (isset($data['packages'])) { + $packages = $data['packages']; + } else { + $packages = $data; + } if (!is_array($packages)) { throw new \UnexpectedValueException('Could not parse package list from the repository'); @@ -74,16 +79,16 @@ class FilesystemRepository extends WritableArrayRepository /** * Writes writable repository. */ - public function write() + public function write($devMode) { - $data = array(); + $data = array('packages' => array(), 'dev' => $devMode); $dumper = new ArrayDumper(); foreach ($this->getCanonicalPackages() as $package) { - $data[] = $dumper->dump($package); + $data['packages'][] = $dumper->dump($package); } - usort($data, function ($a, $b) { + usort($data['packages'], function ($a, $b) { return strcmp($a['name'], $b['name']); }); diff --git a/src/Composer/Repository/WritableArrayRepository.php b/src/Composer/Repository/WritableArrayRepository.php index 041e40562..284f9bcb0 100644 --- a/src/Composer/Repository/WritableArrayRepository.php +++ b/src/Composer/Repository/WritableArrayRepository.php @@ -24,7 +24,7 @@ class WritableArrayRepository extends ArrayRepository implements WritableReposit /** * {@inheritDoc} */ - public function write() + public function write($devMode) { } diff --git a/src/Composer/Repository/WritableRepositoryInterface.php b/src/Composer/Repository/WritableRepositoryInterface.php index 4500005d9..4fb3d0c66 100644 --- a/src/Composer/Repository/WritableRepositoryInterface.php +++ b/src/Composer/Repository/WritableRepositoryInterface.php @@ -23,8 +23,10 @@ interface WritableRepositoryInterface extends RepositoryInterface { /** * Writes repository (f.e. to the disc). + * + * @param bool $devMode Whether dev requirements were included or not in this installation */ - public function write(); + public function write($devMode); /** * Adds package to the repository. diff --git a/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php b/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php index 9c11dc307..8c8c280e8 100644 --- a/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php +++ b/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php @@ -20,7 +20,7 @@ class InstalledFilesystemRepositoryMock extends InstalledFilesystemRepository { } - public function write() + public function write($devMode) { } } diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index be8b0d0a9..4d8d7c103 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -95,11 +95,12 @@ class FilesystemRepositoryTest extends TestCase ->expects($this->once()) ->method('write') ->with(array( - array('name' => 'mypkg', 'type' => 'library', 'version' => '0.1.10', 'version_normalized' => '0.1.10.0'), + 'packages' => array(array('name' => 'mypkg', 'type' => 'library', 'version' => '0.1.10', 'version_normalized' => '0.1.10.0')), + 'dev' => true, )); $repository->addPackage($this->getPackage('mypkg', '0.1.10')); - $repository->write(); + $repository->write(true); } private function createJsonFileMock() From 3f5a986170867e1c06517f7cc82c6db9cf99f1d7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 21 Feb 2019 14:49:06 +0100 Subject: [PATCH 084/321] Show warning in all 400/500 responses if available, fixes #7814 --- .../Repository/ComposerRepository.php | 26 +++---------------- src/Composer/Util/AuthHelper.php | 7 +---- src/Composer/Util/Http/CurlDownloader.php | 15 +++++------ src/Composer/Util/HttpDownloader.php | 22 ++++++++++++++++ src/Composer/Util/RemoteFilesystem.php | 18 ++++++------- 5 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e835e221b..0cdb74bd6 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -962,7 +962,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $data = $response->decodeJson(); - $this->outputWarnings($data); + HttpDownloader::outputWarnings($this->io, $this->url, $data); if ($cacheKey) { if ($storeLastModifiedTime) { @@ -1036,7 +1036,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $data = $response->decodeJson(); - $this->outputWarnings($data); + HttpDownloader::outputWarnings($this->io, $this->url, $data); $lastModifiedDate = $response->getHeader('last-modified'); $response->collect(); @@ -1101,7 +1101,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $data = $response->decodeJson(); - $this->outputWarnings($data); + HttpDownloader::outputWarnings($io, $url, $data); $lastModifiedDate = $response->getHeader('last-modified'); $response->collect(); @@ -1161,24 +1161,4 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito // wipe rootData as it is fully consumed at this point and this saves some memory $this->rootData = true; } - - private function outputWarnings($data) - { - foreach (array('warning', 'info') as $type) { - if (empty($data[$type])) { - continue; - } - - if (!empty($data[$type . '-versions'])) { - $versionParser = new VersionParser(); - $constraint = $versionParser->parseConstraints($data[$type . '-versions']); - $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion())); - if (!$constraint->matches($composer)) { - continue; - } - } - - $this->io->writeError('<'.$type.'>'.ucfirst($type).' from '.$this->url.': '.$data[$type].''); - } - } } diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index 2868d3346..7085f2561 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -71,12 +71,11 @@ class AuthHelper * @param string $origin * @param int $statusCode HTTP status code that triggered this call * @param string|null $reason a message/description explaining why this was called - * @param string $warning an authentication warning returned by the server as {"warning": ".."}, if present * @param string[] $headers * @return array|null containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be * retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json */ - public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $warning = null, $headers = array()) + public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $headers = array()) { $storeAuth = false; $retry = false; @@ -173,10 +172,6 @@ class AuthHelper throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); } - $this->io->overwriteError(''); - if ($warning) { - $this->io->writeError(' '.$warning.''); - } $this->io->writeError(' Authentication required ('.parse_url($url, PHP_URL_HOST).'):'); $username = $this->io->ask(' Username: '); $password = $this->io->askAndHideAnswer(' Password: '); diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index ab0dae91e..1ebc64242 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -20,6 +20,7 @@ use Composer\Util\RemoteFilesystem; use Composer\Util\StreamContextFactory; use Composer\Util\AuthHelper; use Composer\Util\Url; +use Composer\Util\HttpDownloader; use React\Promise\Promise; /** @@ -261,6 +262,10 @@ class CurlDownloader $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); } + if ($response->getStatusCode() >= 400 && $response->getHeader('content-type') === 'application/json') { + HttpDownloader::outputWarnings($this->io, $job['origin'], json_decode($response->getBody(), true)); + } + $result = $this->isAuthenticatedRetryNeeded($job, $response); if ($result['retry']) { if ($job['filename']) { @@ -371,15 +376,7 @@ class CurlDownloader private function isAuthenticatedRetryNeeded(array $job, Response $response) { if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) { - $warning = null; - if ($response->getHeader('content-type') === 'application/json') { - $data = json_decode($response->getBody(), true); - if (!empty($data['warning'])) { - $warning = $data['warning']; - } - } - - $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $warning, $response->getHeaders()); + $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $response->getHeaders()); if ($result['retry']) { return $result; diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 5a5ec14b2..94e7ec0db 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -17,6 +17,8 @@ use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; use Composer\Util\Http\Response; +use Composer\Package\Version\VersionParser; +use Composer\Semver\Constraint\Constraint; use React\Promise\Promise; /** @@ -313,4 +315,24 @@ class HttpDownloader return $resp; } + + public static function outputWarnings(IOInterface $io, $url, $data) + { + foreach (array('warning', 'info') as $type) { + if (empty($data[$type])) { + continue; + } + + if (!empty($data[$type . '-versions'])) { + $versionParser = new VersionParser(); + $constraint = $versionParser->parseConstraints($data[$type . '-versions']); + $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion())); + if (!$constraint->matches($composer)) { + continue; + } + } + + $io->writeError('<'.$type.'>'.ucfirst($type).' from '.$url.': '.$data[$type].''); + } + } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 07eccc791..c6ba4085c 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -16,6 +16,7 @@ use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; +use Composer\Util\HttpDownloader; /** * @author François Pluchino @@ -291,15 +292,12 @@ class RemoteFilesystem if (!empty($http_response_header[0])) { $statusCode = $this->findStatusCode($http_response_header); + if ($statusCode >= 400 && $this->findHeaderValue($http_response_header, 'content-type') === 'application/json') { + HttpDownloader::outputWarnings($this->io, $originUrl, json_decode($result, true)); + } + if (in_array($statusCode, array(401, 403)) && $this->retryAuthFailure) { - $warning = null; - if ($this->findHeaderValue($http_response_header, 'content-type') === 'application/json') { - $data = json_decode($result, true); - if (!empty($data['warning'])) { - $warning = $data['warning']; - } - } - $this->promptAuthAndRetry($statusCode, $this->findStatusMessage($http_response_header), $warning, $http_response_header); + $this->promptAuthAndRetry($statusCode, $this->findStatusMessage($http_response_header), $http_response_header); } } @@ -613,9 +611,9 @@ class RemoteFilesystem } } - protected function promptAuthAndRetry($httpStatus, $reason = null, $warning = null, $headers = array()) + protected function promptAuthAndRetry($httpStatus, $reason = null, $headers = array()) { - $result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $warning, $headers); + $result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $headers); $this->storeAuth = $result['storeAuth']; $this->retry = $result['retry']; From d37642d9f2719316e99ab186ab971e6ccf255bb8 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 21 Feb 2019 14:59:28 +0100 Subject: [PATCH 085/321] Add missing use --- src/Composer/Util/HttpDownloader.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 94e7ec0db..93026ecbe 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -17,6 +17,7 @@ use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; use Composer\Util\Http\Response; +use Composer\Composer; use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\Constraint; use React\Promise\Promise; From f77285916a8c0481a57554ad8615a049660e6b9b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 21 Feb 2019 15:28:50 +0100 Subject: [PATCH 086/321] Clean up temp file on curl request failure and make sure the response body is avaiable on 3xx/4xx/5xx responses --- src/Composer/Util/Http/CurlDownloader.php | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 1ebc64242..989e63d12 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -251,16 +251,20 @@ class CurlDownloader // prepare response object if ($job['filename']) { - fclose($job['bodyHandle']); - $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $job['filename'].'~'); + $contents = $job['filename'].'~'; + if ($statusCode >= 300) { + rewind($job['bodyHandle']); + $contents = stream_get_contents($job['bodyHandle']); + } + $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); } else { rewind($job['bodyHandle']); $contents = stream_get_contents($job['bodyHandle']); - fclose($job['bodyHandle']); $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); $this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG); } + fclose($job['bodyHandle']); if ($response->getStatusCode() >= 400 && $response->getHeader('content-type') === 'application/json') { HttpDownloader::outputWarnings($this->io, $job['origin'], json_decode($response->getBody(), true)); @@ -268,10 +272,6 @@ class CurlDownloader $result = $this->isAuthenticatedRetryNeeded($job, $response); if ($result['retry']) { - if ($job['filename']) { - @unlink($job['filename'].'~'); - } - $this->restartJob($job, $job['url'], array('storeAuth' => $result['storeAuth'])); continue; } @@ -422,6 +422,10 @@ class CurlDownloader private function restartJob(array $job, $url, array $attributes = array()) { + if ($job['filename']) { + @unlink($job['filename'].'~'); + } + $attributes = array_merge($job['attributes'], $attributes); $origin = Url::getOrigin($this->config, $url); @@ -430,6 +434,10 @@ class CurlDownloader private function failResponse(array $job, Response $response, $errorMessage) { + if ($job['filename']) { + @unlink($job['filename'].'~'); + } + return new TransportException('The "'.$job['url'].'" file could not be downloaded ('.$errorMessage.')', $response->getStatusCode()); } From 794234946c3b06e942e3d153ad08a9ba8d3a37df Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 15 Apr 2019 16:23:38 +0200 Subject: [PATCH 087/321] Let curl handle proxy and cipher list itself --- src/Composer/Util/Http/CurlDownloader.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 989e63d12..f7ae28a24 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -51,11 +51,9 @@ class CurlDownloader 'http' => array( 'method' => CURLOPT_CUSTOMREQUEST, 'content' => CURLOPT_POSTFIELDS, - 'proxy' => CURLOPT_PROXY, 'header' => CURLOPT_HTTPHEADER, ), 'ssl' => array( - 'ciphers' => CURLOPT_SSL_CIPHER_LIST, 'cafile' => CURLOPT_CAINFO, 'capath' => CURLOPT_CAPATH, ), From e33560a33f4c137aed8923f254b581dcf61b5aa8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 30 Apr 2019 14:07:46 +0200 Subject: [PATCH 088/321] Fixed 2.0 branch alias --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3a18fe45b..725f211e1 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "2.0-dev" } }, "autoload": { From c35a3c1c071be68fabfb4862dcb7f861d0f053ad Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 May 2019 09:35:13 +0200 Subject: [PATCH 089/321] update composer.lock --- composer.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index c678c96ea..eb15db515 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b078b12b2912d599e0c6904f64def484", + "content-hash": "280f5d5184039085b5f22236d267ae82", "packages": [ { "name": "composer/ca-bundle", @@ -1404,7 +1404,6 @@ "mock", "xunit" ], - "abandoned": true, "time": "2015-10-02T06:51:40+00:00" }, { From b935d1c8129f4f97e7d7a1cc9e5eea64a1d32890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=BDurek?= Date: Fri, 12 Jul 2019 18:34:12 +0200 Subject: [PATCH 090/321] fixed phpstan error --- tests/Composer/Test/Repository/ComposerRepositoryTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 47df3a443..22a58cf4f 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -227,7 +227,9 @@ class ComposerRepositoryTest extends TestCase $repository = new ComposerRepository( array('url' => $repositoryUrl), new NullIO(), - FactoryMock::createConfig() + FactoryMock::createConfig(), + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); $object = new \ReflectionObject($repository); From b5014e9aed08bd143f0f6944a156665fb86841ff Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 12:19:12 +0200 Subject: [PATCH 091/321] Fix use of decodeJson --- src/Composer/Command/DiagnoseCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index c2123e066..dee37502f 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -254,7 +254,7 @@ EOT $protocol = extension_loaded('openssl') ? 'https' : 'http'; try { - $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->parseJson(); + $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->decodeJson(); $hash = reset($json['provider-includes']); $hash = $hash['sha256']; $path = str_replace('%hash%', $hash, key($json['provider-includes'])); @@ -375,7 +375,7 @@ EOT } $url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit'; - $data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->parseJson(); + $data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->decodeJson(); return $data['resources']['core']; } From 76a2c63bf8f2b77f68d1b4252781450d06981487 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 14:00:34 +0200 Subject: [PATCH 092/321] Show best possible version in diagnose command --- src/Composer/Command/DiagnoseCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index dee37502f..3bcc665dc 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -156,7 +156,7 @@ EOT $this->outputResult($this->checkVersion($config)); } - $io->write(sprintf('Composer version: %s', Composer::VERSION)); + $io->write(sprintf('Composer version: %s', Composer::getVersion())); $platformOverrides = $config->get('platform') ?: array(); $platformRepo = new PlatformRepository(array(), $platformOverrides); From f7c1b04a6ccbcf23ed8b8450162abe58da6aa216 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 14:26:42 +0200 Subject: [PATCH 093/321] Improve output when installing packages --- src/Composer/Downloader/ArchiveDownloader.php | 8 +++----- src/Composer/Downloader/PathDownloader.php | 8 ++++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 3c53a086e..96aad4b9a 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -33,16 +33,14 @@ abstract class ArchiveDownloader extends FileDownloader public function install(PackageInterface $package, $path, $output = true) { if ($output) { - $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): Extracting archive"); + } else { + $this->io->writeError('Extracting archive', false); } $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); $fileName = $this->getFileName($package, $path); - if ($output) { - $this->io->writeError(' Extracting archive', true, IOInterface::VERBOSE); - } - try { $this->filesystem->ensureDirectoryExists($temporaryDir); try { diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 23c51398e..e289c0173 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -82,6 +82,8 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $package->getName(), $package->getFullPrettyVersion() )); + } else { + $this->io->writeError('Source already present', false); } return; @@ -163,7 +165,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $fileSystem->mirror($realUrl, $path, $iterator); } - $this->io->writeError(''); + if ($output) { + $this->io->writeError(''); + } } /** @@ -173,7 +177,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter { $realUrl = realpath($package->getDistUrl()); - if (realpath($path) === $realUrl) { + if ($path === $realUrl) { if ($output) { $this->io->writeError(" - Removing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "), source is still present in $path"); } From 898ba6f869c49eb86d2d94c3f3719df05b259301 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 15:53:10 +0200 Subject: [PATCH 094/321] Only empty dir before actually installing packages, fixes #7929 --- src/Composer/Downloader/ArchiveDownloader.php | 2 ++ src/Composer/Downloader/FileDownloader.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 96aad4b9a..be863f1d3 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -38,6 +38,8 @@ abstract class ArchiveDownloader extends FileDownloader $this->io->writeError('Extracting archive', false); } + $this->filesystem->emptyDirectory($path); + $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); $fileName = $this->getFileName($package, $path); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 2c66c23a3..da2955638 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -101,7 +101,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface ); } - $this->filesystem->emptyDirectory($path); $fileName = $this->getFileName($package, $path); $io = $this->io; @@ -229,6 +228,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); } + $this->filesystem->emptyDirectory($path); $this->filesystem->ensureDirectoryExists($path); $this->filesystem->rename($this->getFileName($package, $path), $path . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME)); } From 6a7220fed8b6fa00ad4dd962ac147593b0616131 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 15:57:33 +0200 Subject: [PATCH 095/321] Avoid wiping the whole target package if download of the new one fails, refs #7929 --- src/Composer/Downloader/FileDownloader.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index da2955638..6a08bd67d 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -175,7 +175,9 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $reject = function ($e) use ($io, &$urls, $download, $fileName, $path, $package, &$retries, $filesystem, $self) { // clean up - $filesystem->removeDirectory($path); + if (file_exists($fileName)) { + $filesystem->unlink($fileName); + } $self->clearLastCacheWrite($package); if ($e instanceof TransportException) { From 9ee345ed29985e4e5b1142bf4442910aab8fa684 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 16:33:39 +0200 Subject: [PATCH 096/321] Make sure the directory exists and will not block installation later when downloading --- src/Composer/Downloader/FileDownloader.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 6a08bd67d..ab72bbaed 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -101,6 +101,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface ); } + $this->filesystem->ensureDirectoryExists($path); $fileName = $this->getFileName($package, $path); $io = $this->io; From 675f75c4b47ac4cf2fa9d15c9e4fd14fceed7d1f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 16:41:06 +0200 Subject: [PATCH 097/321] Remove unnecessary config from phpstan --- phpstan/config.neon | 1 - 1 file changed, 1 deletion(-) diff --git a/phpstan/config.neon b/phpstan/config.neon index 9fd155963..0c5dc30b8 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -16,7 +16,6 @@ parameters: - '~^Anonymous function has an unused use \$io\.$~' - '~^Anonymous function has an unused use \$cache\.$~' - '~^Anonymous function has an unused use \$path\.$~' - - '~^Anonymous function has an unused use \$fileName\.$~' # ion cube is not installed - '~^Function ioncube_loader_\w+ not found\.$~' From 63da7c6b2d164cf264cfe456780009ea5119d4f9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 21:39:26 +0200 Subject: [PATCH 098/321] Add install-path to the installed.json for every package, fixes #2174, closes #2424 --- src/Composer/Installer.php | 4 ++-- src/Composer/Repository/FilesystemRepository.php | 11 +++++++++-- .../Repository/WritableArrayRepository.php | 3 ++- .../Repository/WritableRepositoryInterface.php | 3 ++- .../Mock/InstalledFilesystemRepositoryMock.php | 3 ++- tests/Composer/Test/Package/LockerTest.php | 4 ++-- .../Test/Repository/FilesystemRepositoryTest.php | 14 ++++++++++++-- 7 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index ff5961202..9c94e40d9 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -618,7 +618,7 @@ class Installer } if ($this->executeOperations || $this->writeLock) { - $localRepo->write($this->devMode); + $localRepo->write($this->devMode, $this->installationManager); } $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); @@ -630,7 +630,7 @@ class Installer if ($this->executeOperations) { // force source/dist urls to be updated for all packages $this->processPackageUrls($pool, $policy, $localRepo, $repositories); - $localRepo->write($this->devMode); + $localRepo->write($this->devMode, $this->installationManager); } return array(0, $devPackages); diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index f0876dfcc..0b6563dd6 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -15,6 +15,8 @@ namespace Composer\Repository; use Composer\Json\JsonFile; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Installer\InstallationManager; +use Composer\Util\Filesystem; /** * Filesystem repository. @@ -84,13 +86,18 @@ class FilesystemRepository extends WritableArrayRepository /** * Writes writable repository. */ - public function write($devMode) + public function write($devMode, InstallationManager $installationManager) { $data = array('packages' => array(), 'dev' => $devMode); $dumper = new ArrayDumper(); + $fs = new Filesystem(); + $repoDir = dirname($fs->normalizePath($this->file->getPath())); foreach ($this->getCanonicalPackages() as $package) { - $data['packages'][] = $dumper->dump($package); + $pkgArray = $dumper->dump($package); + $path = $installationManager->getInstallPath($package); + $pkgArray['install-path'] = ('' !== $path && null !== $path) ? $fs->findShortestPath($repoDir, $path, true) : null; + $data['packages'][] = $pkgArray; } usort($data['packages'], function ($a, $b) { diff --git a/src/Composer/Repository/WritableArrayRepository.php b/src/Composer/Repository/WritableArrayRepository.php index 284f9bcb0..3580593bb 100644 --- a/src/Composer/Repository/WritableArrayRepository.php +++ b/src/Composer/Repository/WritableArrayRepository.php @@ -13,6 +13,7 @@ namespace Composer\Repository; use Composer\Package\AliasPackage; +use Composer\Installer\InstallationManager; /** * Writable array repository. @@ -24,7 +25,7 @@ class WritableArrayRepository extends ArrayRepository implements WritableReposit /** * {@inheritDoc} */ - public function write($devMode) + public function write($devMode, InstallationManager $installationManager) { } diff --git a/src/Composer/Repository/WritableRepositoryInterface.php b/src/Composer/Repository/WritableRepositoryInterface.php index 4fb3d0c66..c35fdb257 100644 --- a/src/Composer/Repository/WritableRepositoryInterface.php +++ b/src/Composer/Repository/WritableRepositoryInterface.php @@ -13,6 +13,7 @@ namespace Composer\Repository; use Composer\Package\PackageInterface; +use Composer\Installer\InstallationManager; /** * Writable repository interface. @@ -26,7 +27,7 @@ interface WritableRepositoryInterface extends RepositoryInterface * * @param bool $devMode Whether dev requirements were included or not in this installation */ - public function write($devMode); + public function write($devMode, InstallationManager $installationManager); /** * Adds package to the repository. diff --git a/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php b/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php index 8c8c280e8..574cfbd83 100644 --- a/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php +++ b/tests/Composer/Test/Mock/InstalledFilesystemRepositoryMock.php @@ -13,6 +13,7 @@ namespace Composer\Test\Mock; use Composer\Repository\InstalledFilesystemRepository; +use Composer\Installer\InstallationManager; class InstalledFilesystemRepositoryMock extends InstalledFilesystemRepository { @@ -20,7 +21,7 @@ class InstalledFilesystemRepositoryMock extends InstalledFilesystemRepository { } - public function write($devMode) + public function write($devMode, InstallationManager $installationManager) { } } diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index 6b945bab2..bb8eaabde 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -24,7 +24,7 @@ class LockerTest extends TestCase $locker = new Locker( new NullIO, $json, - $this->createRepositoryManagerMock(), + $this->createInstallationManagerMock(), $this->getJsonContent() ); @@ -259,7 +259,7 @@ class LockerTest extends TestCase ->disableOriginalConstructor() ->getMock(); } - + private function createInstallationManagerMock() { $mock = $this->getMockBuilder('Composer\Installer\InstallationManager') diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index 4d8d7c103..97747ebc5 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -82,11 +82,21 @@ class FilesystemRepositoryTest extends TestCase $json = $this->createJsonFileMock(); $repository = new FilesystemRepository($json); + $im = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->disableOriginalConstructor() + ->getMock(); + $im->expects($this->once()) + ->method('getInstallPath') + ->will($this->returnValue('/foo/bar/vendor/woop/woop')); $json ->expects($this->once()) ->method('read') ->will($this->returnValue(array())); + $json + ->expects($this->once()) + ->method('getPath') + ->will($this->returnValue('/foo/bar/vendor/composer/installed.json')); $json ->expects($this->once()) ->method('exists') @@ -95,12 +105,12 @@ class FilesystemRepositoryTest extends TestCase ->expects($this->once()) ->method('write') ->with(array( - 'packages' => array(array('name' => 'mypkg', 'type' => 'library', 'version' => '0.1.10', 'version_normalized' => '0.1.10.0')), + 'packages' => array(array('name' => 'mypkg', 'type' => 'library', 'version' => '0.1.10', 'version_normalized' => '0.1.10.0', 'install-path' => '../woop/woop')), 'dev' => true, )); $repository->addPackage($this->getPackage('mypkg', '0.1.10')); - $repository->write(true); + $repository->write(true, $im); } private function createJsonFileMock() From bfee701f9b601ce0fadfd9982d1b18cfd9728019 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 21:54:41 +0200 Subject: [PATCH 099/321] Deduplicate findHeaderValue code --- src/Composer/Repository/Vcs/GitHubDriver.php | 5 ++- src/Composer/Util/Http/CurlDownloader.php | 1 - src/Composer/Util/Http/Response.php | 38 ++++++++++++-------- src/Composer/Util/RemoteFilesystem.php | 34 ++++-------------- 4 files changed, 33 insertions(+), 45 deletions(-) diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index b5c3fb14a..55940e212 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -19,7 +19,6 @@ use Composer\Cache; use Composer\IO\IOInterface; use Composer\Util\GitHub; use Composer\Util\Http\Response; -use Composer\Util\RemoteFilesystem; /** * @author Jordi Boggiano @@ -346,10 +345,10 @@ class GitHubDriver extends VcsDriver $scopesIssued = array(); $scopesNeeded = array(); if ($headers = $e->getHeaders()) { - if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-OAuth-Scopes')) { + if ($scopes = Response::findHeaderValue($headers, 'X-OAuth-Scopes')) { $scopesIssued = explode(' ', $scopes); } - if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) { + if ($scopes = Response::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) { $scopesNeeded = explode(' ', $scopes); } } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index f7ae28a24..a163290fe 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -16,7 +16,6 @@ use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; -use Composer\Util\RemoteFilesystem; use Composer\Util\StreamContextFactory; use Composer\Util\AuthHelper; use Composer\Util\Url; diff --git a/src/Composer/Util/Http/Response.php b/src/Composer/Util/Http/Response.php index d2774c938..1b4581331 100644 --- a/src/Composer/Util/Http/Response.php +++ b/src/Composer/Util/Http/Response.php @@ -61,20 +61,7 @@ class Response public function getHeader($name) { - $value = null; - foreach ($this->headers as $header) { - if (preg_match('{^'.$name.':\s*(.+?)\s*$}i', $header, $match)) { - $value = $match[1]; - } elseif (preg_match('{^HTTP/}i', $header)) { - // TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary - // - // In case of redirects, headers contains the headers of all responses - // so we reset the flag when a new response is being parsed as we are only interested in the last response - $value = null; - } - } - - return $value; + return self::findHeaderValue($this->headers, $name); } public function getBody() @@ -91,4 +78,27 @@ class Response { $this->request = $this->code = $this->headers = $this->body = null; } + + /** + * @param array $headers array of returned headers like from getLastHeaders() + * @param string $name header name (case insensitive) + * @return string|null + */ + public static function findHeaderValue(array $headers, $name) + { + $value = null; + foreach ($headers as $header) { + if (preg_match('{^'.preg_quote($name).':\s*(.+?)\s*$}i', $header, $match)) { + $value = $match[1]; + } elseif (preg_match('{^HTTP/}i', $header)) { + // TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary + // + // In case of redirects, http_response_headers contains the headers of all responses + // so we reset the flag when a new response is being parsed as we are only interested in the last response + $value = null; + } + } + + return $value; + } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d9803835d..48711815a 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -17,6 +17,7 @@ use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; use Composer\Util\HttpDownloader; +use Composer\Util\Response; /** * @author François Pluchino @@ -143,27 +144,6 @@ class RemoteFilesystem return $this->lastHeaders; } - /** - * @param array $headers array of returned headers like from getLastHeaders() - * @param string $name header name (case insensitive) - * @return string|null - */ - public static function findHeaderValue(array $headers, $name) - { - $value = null; - foreach ($headers as $header) { - if (preg_match('{^'.$name.':\s*(.+?)\s*$}i', $header, $match)) { - $value = $match[1]; - } elseif (preg_match('{^HTTP/}i', $header)) { - // In case of redirects, http_response_headers contains the headers of all responses - // so we reset the flag when a new response is being parsed as we are only interested in the last response - $value = null; - } - } - - return $value; - } - /** * @param array $headers array of returned headers like from getLastHeaders() * @return int|null @@ -294,7 +274,7 @@ class RemoteFilesystem if (!empty($http_response_header[0])) { $statusCode = $this->findStatusCode($http_response_header); - if ($statusCode >= 400 && $this->findHeaderValue($http_response_header, 'content-type') === 'application/json') { + if ($statusCode >= 400 && Response::findHeaderValue($http_response_header, 'content-type') === 'application/json') { HttpDownloader::outputWarnings($this->io, $originUrl, json_decode($result, true)); } @@ -303,7 +283,7 @@ class RemoteFilesystem } } - $contentLength = !empty($http_response_header[0]) ? $this->findHeaderValue($http_response_header, 'content-length') : null; + $contentLength = !empty($http_response_header[0]) ? Response::findHeaderValue($http_response_header, 'content-length') : null; if ($contentLength && Platform::strlen($result) < $contentLength) { // alas, this is not possible via the stream callback because STREAM_NOTIFY_COMPLETED is documented, but not implemented anywhere in PHP $e = new TransportException('Content-Length mismatch, received '.Platform::strlen($result).' bytes out of the expected '.$contentLength); @@ -360,8 +340,8 @@ class RemoteFilesystem $locationHeader = null; if (!empty($http_response_header[0])) { $statusCode = $this->findStatusCode($http_response_header); - $contentType = $this->findHeaderValue($http_response_header, 'content-type'); - $locationHeader = $this->findHeaderValue($http_response_header, 'location'); + $contentType = Response::findHeaderValue($http_response_header, 'content-type'); + $locationHeader = Response::findHeaderValue($http_response_header, 'location'); } // check for bitbucket login page asking to authenticate @@ -417,7 +397,7 @@ class RemoteFilesystem // decode gzip if ($result && extension_loaded('zlib') && substr($fileUrl, 0, 4) === 'http' && !$hasFollowedRedirect) { - $contentEncoding = $this->findHeaderValue($http_response_header, 'content-encoding'); + $contentEncoding = Response::findHeaderValue($http_response_header, 'content-encoding'); $decode = $contentEncoding && 'gzip' === strtolower($contentEncoding); if ($decode) { @@ -700,7 +680,7 @@ class RemoteFilesystem private function handleRedirect(array $http_response_header, array $additionalOptions, $result) { - if ($locationHeader = $this->findHeaderValue($http_response_header, 'location')) { + if ($locationHeader = Response::findHeaderValue($http_response_header, 'location')) { if (parse_url($locationHeader, PHP_URL_SCHEME)) { // Absolute URL; e.g. https://example.com/composer $targetUrl = $locationHeader; From 4dabc17ec1e55d42d9611271d1728dcf9f79d56a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 2 Aug 2019 22:21:52 +0200 Subject: [PATCH 100/321] Fix use statement --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 48711815a..76a2176a8 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -17,7 +17,7 @@ use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; use Composer\Util\HttpDownloader; -use Composer\Util\Response; +use Composer\Util\Http\Response; /** * @author François Pluchino From 53d2ab2253c5f73579ea4b845bfd99f28c15a6cf Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 29 Aug 2019 11:37:23 +0200 Subject: [PATCH 101/321] Split up steps on VCS downloaders to allow doing network operations before touching the filesystem on GitDownloader, fixes #7903 --- src/Composer/Downloader/DownloadManager.php | 72 ++++-- .../Downloader/DownloaderInterface.php | 37 ++- src/Composer/Downloader/FileDownloader.php | 18 +- src/Composer/Downloader/FossilDownloader.php | 12 +- src/Composer/Downloader/GitDownloader.php | 111 ++++++--- src/Composer/Downloader/HgDownloader.php | 12 +- src/Composer/Downloader/PathDownloader.php | 2 +- .../Downloader/PerforceDownloader.php | 10 +- src/Composer/Downloader/SvnDownloader.php | 12 +- src/Composer/Downloader/VcsDownloader.php | 132 +++++++--- src/Composer/Downloader/ZipDownloader.php | 4 +- .../Installer/InstallationManager.php | 58 ++++- src/Composer/Installer/InstallerInterface.php | 50 +++- src/Composer/Installer/LibraryInstaller.php | 25 ++ .../Installer/MetapackageInstaller.php | 16 ++ src/Composer/Installer/NoopInstaller.php | 14 ++ src/Composer/Installer/ProjectInstaller.php | 16 ++ src/Composer/Util/Git.php | 8 +- .../Test/Downloader/FossilDownloaderTest.php | 8 +- .../Test/Downloader/GitDownloaderTest.php | 235 +++++++++--------- .../Test/Downloader/HgDownloaderTest.php | 6 + .../Test/Downloader/ZipDownloaderTest.php | 2 +- 22 files changed, 622 insertions(+), 238 deletions(-) diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index a23c167b5..794998bb2 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -165,9 +165,9 @@ class DownloadManager /** * Downloads package into target dir. * - * @param PackageInterface $package package instance - * @param string $targetDir target dir - * @param PackageInterface $prevPackage previous package instance in case of updates + * @param PackageInterface $package package instance + * @param string $targetDir target dir + * @param PackageInterface|null $prevPackage previous package instance in case of updates * * @return PromiseInterface * @throws \InvalidArgumentException if package have no urls to download from @@ -182,7 +182,7 @@ class DownloadManager $io = $this->io; $self = $this; - $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download) { + $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download, $prevPackage) { $source = array_shift($sources); if ($retry) { $io->writeError(' Now trying to download from ' . $source . ''); @@ -214,7 +214,7 @@ class DownloadManager }; try { - $result = $downloader->download($package, $targetDir); + $result = $downloader->download($package, $targetDir, $prevPackage); } catch (\Exception $e) { return $handleError($e); } @@ -232,12 +232,31 @@ class DownloadManager return $download(); } + /** + * Prepares an operation execution + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param string $targetDir target dir + * @param PackageInterface|null $prevPackage previous package instance in case of updates + * + * @return PromiseInterface|null + */ + public function prepare($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) + { + $downloader = $this->getDownloaderForPackage($package); + if ($downloader) { + return $downloader->prepare($type, $package, $targetDir, $prevPackage); + } + } + /** * Installs package into target dir. * * @param PackageInterface $package package instance * @param string $targetDir target dir * + * @return PromiseInterface|null * @throws \InvalidArgumentException if package have no urls to download from * @throws \RuntimeException */ @@ -245,7 +264,7 @@ class DownloadManager { $downloader = $this->getDownloaderForPackage($package); if ($downloader) { - $downloader->install($package, $targetDir); + return $downloader->install($package, $targetDir); } } @@ -256,6 +275,7 @@ class DownloadManager * @param PackageInterface $target target package version * @param string $targetDir target dir * + * @return PromiseInterface|null * @throws \InvalidArgumentException if initial package is not installed */ public function update(PackageInterface $initial, PackageInterface $target, $targetDir) @@ -270,17 +290,14 @@ class DownloadManager // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed if (!$downloader) { - $initialDownloader->remove($initial, $targetDir); - return; + return $initialDownloader->remove($initial, $targetDir); } $initialType = $this->getDownloaderType($initialDownloader); $targetType = $this->getDownloaderType($downloader); if ($initialType === $targetType) { try { - $downloader->update($initial, $target, $targetDir); - - return; + return $downloader->update($initial, $target, $targetDir); } catch (\RuntimeException $e) { if (!$this->io->isInteractive()) { throw $e; @@ -294,8 +311,15 @@ class DownloadManager // if downloader type changed, or update failed and user asks for reinstall, // we wipe the dir and do a new install instead of updating it - $initialDownloader->remove($initial, $targetDir); - $this->install($target, $targetDir); + $promise = $initialDownloader->remove($initial, $targetDir); + if ($promise) { + $self = $this; + return $promise->then(function ($res) use ($self, $target, $targetDir) { + return $self->install($target, $targetDir); + }); + } + + return $this->install($target, $targetDir); } /** @@ -303,12 +327,32 @@ class DownloadManager * * @param PackageInterface $package package instance * @param string $targetDir target dir + * + * @return PromiseInterface|null */ public function remove(PackageInterface $package, $targetDir) { $downloader = $this->getDownloaderForPackage($package); if ($downloader) { - $downloader->remove($package, $targetDir); + return $downloader->remove($package, $targetDir); + } + } + + /** + * Cleans up a failed operation + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param string $targetDir target dir + * @param PackageInterface|null $prevPackage previous package instance in case of updates + * + * @return PromiseInterface|null + */ + public function cleanup($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) + { + $downloader = $this->getDownloaderForPackage($package); + if ($downloader) { + return $downloader->cleanup($type, $package, $targetDir, $prevPackage); } } diff --git a/src/Composer/Downloader/DownloaderInterface.php b/src/Composer/Downloader/DownloaderInterface.php index 2074b16da..01e7f95c8 100644 --- a/src/Composer/Downloader/DownloaderInterface.php +++ b/src/Composer/Downloader/DownloaderInterface.php @@ -31,14 +31,30 @@ interface DownloaderInterface public function getInstallationSource(); /** - * This should do any network-related tasks to prepare for install/update + * This should do any network-related tasks to prepare for an upcoming install/update * * @return PromiseInterface|null */ - public function download(PackageInterface $package, $path); + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null); /** - * Downloads specific package into specific folder. + * Do anything that needs to be done between all downloads have been completed and the actual operation is executed + * + * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore + * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or + * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can + * be undone as much as possible. + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param string $path download path + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null + */ + public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null); + + /** + * Installs specific package into specific folder. * * @param PackageInterface $package package instance * @param string $path download path @@ -61,4 +77,19 @@ interface DownloaderInterface * @param string $path download path */ public function remove(PackageInterface $package, $path); + + /** + * Do anything to cleanup changes applied in the prepare or install/update/uninstall steps + * + * Note that cleanup will be called for all packages regardless if they failed an operation or not, to give + * all installers a change to cleanup things they did previously, so you need to keep track of changes + * applied in the installer/downloader themselves. + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param string $path download path + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null + */ + public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null); } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index ab72bbaed..20d21804d 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -84,7 +84,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface /** * {@inheritDoc} */ - public function download(PackageInterface $package, $path, $output = true) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { if (!$package->getDistUrl()) { throw new \InvalidArgumentException('The given package is missing url information'); @@ -222,6 +222,20 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface return $download(); } + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) + { + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) + { + } + /** * {@inheritDoc} */ @@ -336,7 +350,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $e = null; try { - $res = $this->download($package, $targetDir.'_compare', false); + $res = $this->download($package, $targetDir.'_compare', null, false); $this->httpDownloader->wait(); $res = $this->install($package, $targetDir.'_compare', false); diff --git a/src/Composer/Downloader/FossilDownloader.php b/src/Composer/Downloader/FossilDownloader.php index a814f89b7..be7c987b3 100644 --- a/src/Composer/Downloader/FossilDownloader.php +++ b/src/Composer/Downloader/FossilDownloader.php @@ -23,7 +23,15 @@ class FossilDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doInstall(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + + } + + /** + * {@inheritDoc} + */ + protected function doInstall(PackageInterface $package, $path, $url) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); @@ -49,7 +57,7 @@ class FossilDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index f698981fe..300749ca8 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -29,6 +29,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface private $hasStashedChanges = false; private $hasDiscardedChanges = false; private $gitUtil; + private $cachedPackages = array(); public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null) { @@ -39,7 +40,28 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface /** * {@inheritDoc} */ - public function doInstall(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + GitUtil::cleanEnv(); + + $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/'; + $gitVersion = $this->gitUtil->getVersion(); + + // --dissociate option is only available since git 2.3.0-rc0 + if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { + $this->io->writeError(" - Syncing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") into cache"); + $this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG); + $ref = $package->getSourceReference(); + if ($this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref) && is_dir($cachePath)) { + $this->cachedPackages[$package->getId()][$ref] = true; + } + } + } + + /** + * {@inheritDoc} + */ + protected function doInstall(PackageInterface $package, $path, $url) { GitUtil::cleanEnv(); $path = $this->normalizePath($path); @@ -47,26 +69,20 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $ref = $package->getSourceReference(); $flag = Platform::isWindows() ? '/D ' : ''; - // --dissociate option is only available since git 2.3.0-rc0 - $gitVersion = $this->gitUtil->getVersion(); - $msg = "Cloning ".$this->getShortHash($ref); - - $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer'; - if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { - $this->io->writeError('', true, IOInterface::DEBUG); - $this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG); - try { - $this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref); - if (is_dir($cachePath)) { - $command = - 'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% ' - . '&& cd '.$flag.'%path% ' - . '&& git remote set-url origin %url% && git remote add composer %url%'; - $msg = "Cloning ".$this->getShortHash($ref).' from cache'; - } - } catch (\RuntimeException $e) { + if (!empty($this->cachedPackages[$package->getId()][$ref])) { + $msg = "Cloning ".$this->getShortHash($ref).' from cache'; + $command = + 'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% ' + . '&& cd '.$flag.'%path% ' + . '&& git remote set-url origin %url% && git remote add composer %url%'; + } else { + $msg = "Cloning ".$this->getShortHash($ref); + $command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer'; + if (getenv('COMPOSER_DISABLE_NETWORK')) { + throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting'); } } + $this->io->writeError($msg); $commandCallable = function ($url) use ($path, $command, $cachePath) { @@ -99,13 +115,51 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { GitUtil::cleanEnv(); + $path = $this->normalizePath($path); if (!$this->hasMetadataRepository($path)) { throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } + $cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/'; + $ref = $target->getSourceReference(); + $flag = Platform::isWindows() ? '/D ' : ''; + + if (!empty($this->cachedPackages[$target->getId()][$ref])) { + $msg = "Checking out ".$this->getShortHash($ref).' from cache'; + $command = 'git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer); git remote set-url composer %url%'; + } else { + $msg = "Checking out ".$this->getShortHash($ref); + $command = 'git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer)'; + if (getenv('COMPOSER_DISABLE_NETWORK')) { + throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting'); + } + } + + $this->io->writeError($msg); + + $commandCallable = function ($url) use ($ref, $command, $cachePath) { + return str_replace( + array('%url%', '%ref%', '%cachePath%'), + array( + ProcessExecutor::escape($url), + ProcessExecutor::escape($ref.'^{commit}'), + ProcessExecutor::escape($cachePath), + ), + $command + ); + }; + + $this->gitUtil->runCommand($commandCallable, $url, $path); + if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) { + if ($target->getDistReference() === $target->getSourceReference()) { + $target->setDistReference($newRef); + } + $target->setSourceReference($newRef); + } + $updateOriginUrl = false; if ( 0 === $this->process->execute('git remote -v', $output, $path) @@ -116,23 +170,6 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $updateOriginUrl = true; } } - - $ref = $target->getSourceReference(); - $this->io->writeError(" Checking out ".$this->getShortHash($ref)); - $command = 'git remote set-url composer %s && git rev-parse --quiet --verify %s || (git fetch composer && git fetch --tags composer)'; - - $commandCallable = function ($url) use ($command, $ref) { - return sprintf($command, ProcessExecutor::escape($url), ProcessExecutor::escape($ref.'^{commit}')); - }; - - $this->gitUtil->runCommand($commandCallable, $url, $path); - if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) { - if ($target->getDistReference() === $target->getSourceReference()) { - $target->setDistReference($newRef); - } - $target->setSourceReference($newRef); - } - if ($updateOriginUrl) { $this->updateOriginUrl($path, $target->getSourceUrl()); } diff --git a/src/Composer/Downloader/HgDownloader.php b/src/Composer/Downloader/HgDownloader.php index add381a75..91144a13d 100644 --- a/src/Composer/Downloader/HgDownloader.php +++ b/src/Composer/Downloader/HgDownloader.php @@ -24,7 +24,15 @@ class HgDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doInstall(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + + } + + /** + * {@inheritDoc} + */ + protected function doInstall(PackageInterface $package, $path, $url) { $hgUtils = new HgUtils($this->io, $this->config, $this->process); @@ -44,7 +52,7 @@ class HgDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { $hgUtils = new HgUtils($this->io, $this->config, $this->process); diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index e289c0173..b84396416 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -37,7 +37,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter /** * {@inheritdoc} */ - public function download(PackageInterface $package, $path, $output = true) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { $url = $package->getDistUrl(); $realUrl = realpath($url); diff --git a/src/Composer/Downloader/PerforceDownloader.php b/src/Composer/Downloader/PerforceDownloader.php index 777866714..8be866929 100644 --- a/src/Composer/Downloader/PerforceDownloader.php +++ b/src/Composer/Downloader/PerforceDownloader.php @@ -24,6 +24,14 @@ class PerforceDownloader extends VcsDownloader /** @var Perforce */ protected $perforce; + /** + * {@inheritDoc} + */ + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + + } + /** * {@inheritDoc} */ @@ -76,7 +84,7 @@ class PerforceDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { $this->doInstall($target, $path, $url); } diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index 0aae163c6..35f01eb68 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -28,7 +28,15 @@ class SvnDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doInstall(PackageInterface $package, $path, $url) + protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) + { + + } + + /** + * {@inheritDoc} + */ + protected function doInstall(PackageInterface $package, $path, $url) { SvnUtil::cleanEnv(); $ref = $package->getSourceReference(); @@ -48,7 +56,7 @@ class SvnDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { SvnUtil::cleanEnv(); $ref = $target->getSourceReference(); diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index b87f6433a..15bf55072 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -54,9 +54,57 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa /** * {@inheritDoc} */ - public function download(PackageInterface $package, $path) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null) { - // noop for now, ideally we would do a git fetch already here, or make sure the cached git repo is synced, etc. + if (!$package->getSourceReference()) { + throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); + } + + $urls = $this->prepareUrls($package->getSourceUrls()); + + while ($url = array_shift($urls)) { + try { + return $this->doDownload($package, $path, $url, $prevPackage); + } catch (\Exception $e) { + // rethrow phpunit exceptions to avoid hard to debug bug failures + if ($e instanceof \PHPUnit_Framework_Exception) { + throw $e; + } + if ($this->io->isDebug()) { + $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage()); + } elseif (count($urls)) { + $this->io->writeError(' Failed, trying the next URL'); + } + if (!count($urls)) { + throw $e; + } + } + } + } + + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) + { + if ($type === 'update') { + $this->cleanChanges($prevPackage, $path, true); + } elseif ($type === 'install') { + $this->filesystem->emptyDirectory($path); + } elseif ($type === 'uninstall') { + $this->cleanChanges($package, $path, false); + } + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) + { + if ($type === 'update') { + // TODO keep track of whether prepare was called for this package + $this->reapplyChanges($path); + } } /** @@ -69,32 +117,10 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa } $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): ", false); - $this->filesystem->emptyDirectory($path); - $urls = $package->getSourceUrls(); + $urls = $this->prepareUrls($package->getSourceUrls()); while ($url = array_shift($urls)) { try { - if (Filesystem::isLocalPath($url)) { - // realpath() below will not understand - // url that starts with "file://" - $needle = 'file://'; - $isFileProtocol = false; - if (0 === strpos($url, $needle)) { - $url = substr($url, strlen($needle)); - $isFileProtocol = true; - } - - // realpath() below will not understand %20 spaces etc. - if (false !== strpos($url, '%')) { - $url = rawurldecode($url); - } - - $url = realpath($url); - - if ($isFileProtocol) { - $url = $needle . $url; - } - } $this->doInstall($package, $path, $url); break; } catch (\Exception $e) { @@ -141,15 +167,11 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); - $this->cleanChanges($initial, $path, true); - $urls = $target->getSourceUrls(); + $urls = $this->prepareUrls($target->getSourceUrls()); $exception = null; while ($url = array_shift($urls)) { try { - if (Filesystem::isLocalPath($url)) { - $url = realpath($url); - } $this->doUpdate($initial, $target, $path, $url); $exception = null; @@ -167,8 +189,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa } } - $this->reapplyChanges($path); - // print the commit logs if in verbose mode and VCS metadata is present // because in case of missing metadata code would trigger another exception if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) { @@ -204,7 +224,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa public function remove(PackageInterface $package, $path) { $this->io->writeError(" - Removing " . $package->getName() . " (" . $package->getPrettyVersion() . ")"); - $this->cleanChanges($package, $path, false); if (!$this->filesystem->removeDirectory($path)) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } @@ -243,7 +262,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa } /** - * Guarantee that no changes have been made to the local copy + * Reapply previously stashes changes if applicable, only called after an update (regardless if successful or not) * * @param string $path * @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly @@ -252,12 +271,26 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa { } + /** + * Downloads data needed to run an install/update later + * + * @param PackageInterface $package package instance + * @param string $path download path + * @param string $url package url + * @param PackageInterface|null $prevPackage previous package (in case of an update) + * + * @return PromiseInterface|null + */ + abstract protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null); + /** * Downloads specific package into specific folder. * * @param PackageInterface $package package instance * @param string $path download path * @param string $url package url + * + * @return PromiseInterface|null */ abstract protected function doInstall(PackageInterface $package, $path, $url); @@ -268,6 +301,8 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @param PackageInterface $target updated package * @param string $path download path * @param string $url package url + * + * @return PromiseInterface|null */ abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url); @@ -289,4 +324,33 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @return bool */ abstract protected function hasMetadataRepository($path); + + private function prepareUrls(array $urls) + { + foreach ($urls as $index => $url) { + if (Filesystem::isLocalPath($url)) { + // realpath() below will not understand + // url that starts with "file://" + $fileProtocol = 'file://'; + $isFileProtocol = false; + if (0 === strpos($url, $fileProtocol)) { + $url = substr($url, strlen($fileProtocol)); + $isFileProtocol = true; + } + + // realpath() below will not understand %20 spaces etc. + if (false !== strpos($url, '%')) { + $url = rawurldecode($url); + } + + $urls[$index] = realpath($url); + + if ($isFileProtocol) { + $urls[$index] = $fileProtocol . $urls[$index]; + } + } + } + + return $urls; + } } diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index bd8d3b499..160bae1d6 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -47,7 +47,7 @@ class ZipDownloader extends ArchiveDownloader /** * {@inheritDoc} */ - public function download(PackageInterface $package, $path, $output = true) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { if (null === self::$hasSystemUnzip) { $finder = new ExecutableFinder; @@ -76,7 +76,7 @@ class ZipDownloader extends ArchiveDownloader } } - return parent::download($package, $path, $output); + return parent::download($package, $path, $prevPackage, $output); } /** diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index ce10dc4da..f018c0a31 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -177,11 +177,52 @@ class InstallationManager $promise = $installer->download($target, $operation->getInitialPackage()); } - if (isset($promise)) { + if (!empty($promise)) { $this->loop->wait(array($promise)); } - $this->$method($repo, $operation); + $e = null; + try { + if ($method === 'install' || $method === 'uninstall') { + $package = $operation->getPackage(); + $installer = $this->getInstaller($package->getType()); + $promise = $installer->prepare($method, $package); + } elseif ($method === 'update') { + $target = $operation->getTargetPackage(); + $targetType = $target->getType(); + $installer = $this->getInstaller($targetType); + $promise = $installer->prepare('update', $target, $operation->getInitialPackage()); + } + + if (!empty($promise)) { + $this->loop->wait(array($promise)); + } + + $promise = $this->$method($repo, $operation); + if (!empty($promise)) { + $this->loop->wait(array($promise)); + } + } catch (\Exception $e) { + } + + if ($method === 'install' || $method === 'uninstall') { + $package = $operation->getPackage(); + $installer = $this->getInstaller($package->getType()); + $promise = $installer->cleanup($method, $package); + } elseif ($method === 'update') { + $target = $operation->getTargetPackage(); + $targetType = $target->getType(); + $installer = $this->getInstaller($targetType); + $promise = $installer->cleanup('update', $target, $operation->getInitialPackage()); + } + + if (!empty($promise)) { + $this->loop->wait(array($promise)); + } + + if ($e) { + throw $e; + } } /** @@ -194,8 +235,10 @@ class InstallationManager { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); - $installer->install($repo, $package); + $promise = $installer->install($repo, $package); $this->markForNotification($package); + + return $promise; } /** @@ -214,13 +257,15 @@ class InstallationManager if ($initialType === $targetType) { $installer = $this->getInstaller($initialType); - $installer->update($repo, $initial, $target); + $promise = $installer->update($repo, $initial, $target); $this->markForNotification($target); } else { $this->getInstaller($initialType)->uninstall($repo, $initial); $installer = $this->getInstaller($targetType); - $installer->install($repo, $target); + $promise = $installer->install($repo, $target); } + + return $promise; } /** @@ -233,7 +278,8 @@ class InstallationManager { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); - $installer->uninstall($repo, $package); + + return $installer->uninstall($repo, $package); } /** diff --git a/src/Composer/Installer/InstallerInterface.php b/src/Composer/Installer/InstallerInterface.php index 310c5fcfc..cc4bef7e9 100644 --- a/src/Composer/Installer/InstallerInterface.php +++ b/src/Composer/Installer/InstallerInterface.php @@ -46,26 +46,43 @@ interface InstallerInterface /** * Downloads the files needed to later install the given package. * - * @param PackageInterface $package package instance - * @param PackageInterface $prevPackage previous package instance in case of an update + * @param PackageInterface $package package instance + * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null */ public function download(PackageInterface $package, PackageInterface $prevPackage = null); + /** + * Do anything that needs to be done between all downloads have been completed and the actual operation is executed + * + * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore + * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or + * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can + * be undone as much as possible. + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null); + /** * Installs specific package. * - * @param InstalledRepositoryInterface $repo repository in which to check - * @param PackageInterface $package package instance + * @param InstalledRepositoryInterface $repo repository in which to check + * @param PackageInterface $package package instance + * @return PromiseInterface|null */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package); /** * Updates specific package. * - * @param InstalledRepositoryInterface $repo repository in which to check - * @param PackageInterface $initial already installed package version - * @param PackageInterface $target updated version + * @param InstalledRepositoryInterface $repo repository in which to check + * @param PackageInterface $initial already installed package version + * @param PackageInterface $target updated version + * @return PromiseInterface|null * * @throws InvalidArgumentException if $initial package is not installed */ @@ -74,11 +91,26 @@ interface InstallerInterface /** * Uninstalls specific package. * - * @param InstalledRepositoryInterface $repo repository in which to check - * @param PackageInterface $package package instance + * @param InstalledRepositoryInterface $repo repository in which to check + * @param PackageInterface $package package instance + * @return PromiseInterface|null */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package); + /** + * Do anything to cleanup changes applied in the prepare or install/update/uninstall steps + * + * Note that cleanup will be called for all packages regardless if they failed an operation or not, to give + * all installers a change to cleanup things they did previously, so you need to keep track of changes + * applied in the installer/downloader themselves. + * + * @param string $type one of install/update/uninstall + * @param PackageInterface $package package instance + * @param PackageInterface $prevPackage previous package instance in case of an update + * @return PromiseInterface|null + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null); + /** * Returns the installation path of a package * diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index a89553b1b..5e99e1f47 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -85,6 +85,9 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath); } + /** + * {@inheritDoc} + */ public function download(PackageInterface $package, PackageInterface $prevPackage = null) { $this->initializeVendorDir(); @@ -93,6 +96,28 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface return $this->downloadManager->download($package, $downloadPath, $prevPackage); } + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->initializeVendorDir(); + $downloadPath = $this->getInstallPath($package); + + return $this->downloadManager->prepare($type, $package, $downloadPath, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->initializeVendorDir(); + $downloadPath = $this->getInstallPath($package); + + return $this->downloadManager->cleanup($type, $package, $downloadPath, $prevPackage); + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Installer/MetapackageInstaller.php b/src/Composer/Installer/MetapackageInstaller.php index b47c00740..1ed6beb71 100644 --- a/src/Composer/Installer/MetapackageInstaller.php +++ b/src/Composer/Installer/MetapackageInstaller.php @@ -55,6 +55,22 @@ class MetapackageInstaller implements InstallerInterface // noop } + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + // noop + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + // noop + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Installer/NoopInstaller.php b/src/Composer/Installer/NoopInstaller.php index 51df3c305..4fe581ff5 100644 --- a/src/Composer/Installer/NoopInstaller.php +++ b/src/Composer/Installer/NoopInstaller.php @@ -47,6 +47,20 @@ class NoopInstaller implements InstallerInterface { } + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Installer/ProjectInstaller.php b/src/Composer/Installer/ProjectInstaller.php index 350b220f5..069c741ec 100644 --- a/src/Composer/Installer/ProjectInstaller.php +++ b/src/Composer/Installer/ProjectInstaller.php @@ -71,6 +71,22 @@ class ProjectInstaller implements InstallerInterface return $this->downloadManager->download($package, $installPath, $prevPackage); } + /** + * {@inheritDoc} + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage); + } + + /** + * {@inheritDoc} + */ + public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + $this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage); + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 74e5c286f..48e91ba84 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -224,6 +224,10 @@ class Git public function syncMirror($url, $dir) { + if (getenv('COMPOSER_DISABLE_NETWORK')) { + return false; + } + // update the repo if it is a valid git repository if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { try { @@ -260,9 +264,7 @@ class Git } } - $this->syncMirror($url, $dir); - - return false; + return $this->syncMirror($url, $dir); } private function isAuthenticationFailure($url, &$match) diff --git a/tests/Composer/Test/Downloader/FossilDownloaderTest.php b/tests/Composer/Test/Downloader/FossilDownloaderTest.php index 9ab7b6b84..4ec8fed45 100644 --- a/tests/Composer/Test/Downloader/FossilDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FossilDownloaderTest.php @@ -48,7 +48,7 @@ class FossilDownloaderTest extends TestCase /** * @expectedException \InvalidArgumentException */ - public function testDownloadForPackageWithoutSourceReference() + public function testInstallForPackageWithoutSourceReference() { $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->once()) @@ -59,7 +59,7 @@ class FossilDownloaderTest extends TestCase $downloader->install($packageMock, '/path'); } - public function testDownload() + public function testInstall() { $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -104,7 +104,9 @@ class FossilDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); + $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock); $downloader->update($initialPackageMock, $sourcePackageMock, '/path'); + $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock); } public function testUpdate() @@ -140,7 +142,9 @@ class FossilDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testRemove() diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index b9a85a666..bf1402186 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\Test\TestCase; use Composer\Util\Filesystem; use Composer\Util\Platform; +use Prophecy\Argument; class GitDownloaderTest extends TestCase { @@ -79,7 +80,10 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); + $downloader->download($packageMock, '/path'); + $downloader->prepare('install', $packageMock, '/path'); $downloader->install($packageMock, '/path'); + $downloader->cleanup('install', $packageMock, '/path'); } public function testDownload() @@ -130,7 +134,10 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); + $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); } public function testDownloadWithCache() @@ -195,7 +202,10 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, $config, $processExecutor); + $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); @rmdir($cachePath); } @@ -265,7 +275,10 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); + $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); } public function pushUrlProvider() @@ -329,12 +342,12 @@ class GitDownloaderTest extends TestCase $config->merge(array('config' => array('github-protocols' => $protocols))); $downloader = $this->getDownloaderMock(null, $config, $processExecutor); + $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); } - /** - * @expectedException \RuntimeException - */ public function testDownloadThrowsRuntimeExceptionIfGitCommandFails() { $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); @@ -359,8 +372,20 @@ class GitDownloaderTest extends TestCase ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(1)); - $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->install($packageMock, 'composerPath'); + // not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe + try { + $downloader = $this->getDownloaderMock(null, null, $processExecutor); + $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); + $this->fail('This test should throw'); + } catch (\RuntimeException $e) { + if ('RuntimeException' !== get_class($e)) { + throw $e; + } + $this->assertEquals('RuntimeException', get_class($e)); + } } /** @@ -375,7 +400,10 @@ class GitDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); + $downloader->download($sourcePackageMock, '/path', $initialPackageMock); + $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock); $downloader->update($initialPackageMock, $sourcePackageMock, '/path'); + $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock); } public function testUpdate() @@ -392,39 +420,22 @@ class GitDownloaderTest extends TestCase $packageMock->expects($this->any()) ->method('getVersion') ->will($this->returnValue('1.0.0.0')); - $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); - $processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(1)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(2)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(3)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(4)) - ->method('execute') - ->with($this->equalTo($this->winCompat($expectedGitUpdateCommand)), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(5)) - ->method('execute') - ->with($this->equalTo('git branch -r')) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(6)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) - ->will($this->returnValue(0)); + + $process = $this->prophesize('Composer\Util\ProcessExecutor'); + $process->execute($this->winCompat('git --version'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git show-ref --head -d'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git status --porcelain --untracked-files=no'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git remote -v'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0); + $process->execute($expectedGitUpdateCommand, null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled(); + $process->execute($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled(); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); - $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); + $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal()); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testUpdateWithNewRepoUrl() @@ -444,27 +455,20 @@ class GitDownloaderTest extends TestCase $packageMock->expects($this->any()) ->method('getVersion') ->will($this->returnValue('1.0.0.0')); + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor->expects($this->at(0)) ->method('execute') - ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) + ->with($this->equalTo($this->winCompat("git --version"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) ->method('execute') - ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnCallback(function ($cmd, &$output, $cwd) { - $output = 'origin https://github.com/old/url (fetch) -origin https://github.com/old/url (push) -composer https://github.com/old/url (fetch) -composer https://github.com/old/url (push) -'; - - return 0; - })); + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) + ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') ->with($this->equalTo($this->winCompat("git remote -v"))) @@ -482,26 +486,41 @@ composer https://github.com/old/url (push) ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(7)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git remote -v"))) + ->will($this->returnCallback(function ($cmd, &$output, $cwd) { + $output = 'origin https://github.com/old/url (fetch) +origin https://github.com/old/url (push) +composer https://github.com/old/url (fetch) +composer https://github.com/old/url (push) +'; + + return 0; + })); + $processExecutor->expects($this->at(8)) ->method('execute') ->with($this->equalTo($this->winCompat("git remote set-url origin 'https://github.com/composer/composer'")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(8)) + $processExecutor->expects($this->at(9)) ->method('execute') ->with($this->equalTo($this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) ->will($this->returnValue(0)); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } /** * @group failing - * @expectedException \RuntimeException */ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails() { $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); + $expectedGitUpdateCommand2 = $this->winCompat("git remote set-url composer 'git@github.com:composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) @@ -513,36 +532,38 @@ composer https://github.com/old/url (push) $packageMock->expects($this->any()) ->method('getVersion') ->will($this->returnValue('1.0.0.0')); - $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); - $processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(1)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(2)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(3)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(4)) - ->method('execute') - ->with($this->equalTo($expectedGitUpdateCommand)) - ->will($this->returnValue(1)); + + $process = $this->prophesize('Composer\Util\ProcessExecutor'); + $process->execute($this->winCompat('git --version'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git show-ref --head -d'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git status --porcelain --untracked-files=no'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git remote -v'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0); + $process->execute($expectedGitUpdateCommand, null, $this->winCompat($this->workingDir))->willReturn(1)->shouldBeCalled(); + $process->execute($expectedGitUpdateCommand2, null, $this->winCompat($this->workingDir))->willReturn(1)->shouldBeCalled(); + $process->getErrorOutput()->willReturn(''); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); - $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); - $downloader->update($packageMock, $packageMock, $this->workingDir); + + // not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe + try { + $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal()); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); + $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); + $this->fail('This test should throw'); + } catch (\RuntimeException $e) { + if ('RuntimeException' !== get_class($e)) { + throw $e; + } + $this->assertEquals('RuntimeException', get_class($e)); + } } public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover() { - $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); + $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); @@ -554,52 +575,24 @@ composer https://github.com/old/url (push) ->will($this->returnValue('1.0.0.0')); $packageMock->expects($this->any()) ->method('getSourceUrls') - ->will($this->returnValue(array('/foo/bar', 'https://github.com/composer/composer'))); - $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); - $processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git show-ref --head -d"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(1)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(2)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(3)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(4)) - ->method('execute') - ->with($this->equalTo($expectedFirstGitUpdateCommand)) - ->will($this->returnValue(1)); - $processExecutor->expects($this->at(6)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git --version"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(7)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(8)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git remote -v"))) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(9)) - ->method('execute') - ->with($this->equalTo($expectedSecondGitUpdateCommand)) - ->will($this->returnValue(0)); - $processExecutor->expects($this->at(11)) - ->method('execute') - ->with($this->equalTo($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --")), $this->equalTo(null), $this->equalTo($this->winCompat($this->workingDir))) - ->will($this->returnValue(0)); + ->will($this->returnValue(array(Platform::isWindows() ? 'C:\\' : '/', 'https://github.com/composer/composer'))); + + $process = $this->prophesize('Composer\Util\ProcessExecutor'); + $process->execute($this->winCompat('git --version'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git show-ref --head -d'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git status --porcelain --untracked-files=no'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git remote -v'), Argument::cetera())->willReturn(0); + $process->execute($this->winCompat('git branch -r'), Argument::cetera())->willReturn(0); + $process->execute($expectedFirstGitUpdateCommand, Argument::cetera())->willReturn(1)->shouldBeCalled(); + $process->execute($expectedSecondGitUpdateCommand, Argument::cetera())->willReturn(0)->shouldBeCalled(); + $process->execute($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled(); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); - $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); + $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal()); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testDowngradeShowsAppropriateMessage() @@ -644,7 +637,10 @@ composer https://github.com/old/url (push) $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock($ioMock, null, $processExecutor); + $downloader->download($newPackage, $this->workingDir, $oldPackage); + $downloader->prepare('update', $newPackage, $this->workingDir, $oldPackage); $downloader->update($oldPackage, $newPackage, $this->workingDir); + $downloader->cleanup('update', $newPackage, $this->workingDir, $oldPackage); } public function testNotUsingDowngradingWithReferences() @@ -679,11 +675,14 @@ composer https://github.com/old/url (push) $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $ioMock->expects($this->at(0)) ->method('writeError') - ->with($this->stringContains('updating')); + ->with($this->stringContains('Updating')); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock($ioMock, null, $processExecutor); + $downloader->download($newPackage, $this->workingDir, $oldPackage); + $downloader->prepare('update', $newPackage, $this->workingDir, $oldPackage); $downloader->update($oldPackage, $newPackage, $this->workingDir); + $downloader->cleanup('update', $newPackage, $this->workingDir, $oldPackage); } public function testRemove() @@ -703,7 +702,9 @@ composer https://github.com/old/url (push) ->will($this->returnValue(true)); $downloader = $this->getDownloaderMock(null, null, $processExecutor, $filesystem); + $downloader->prepare('uninstall', $packageMock, 'composerPath'); $downloader->remove($packageMock, 'composerPath'); + $downloader->cleanup('uninstall', $packageMock, 'composerPath'); } public function testGetInstallationSource() diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php index a4219d143..c69d497ce 100644 --- a/tests/Composer/Test/Downloader/HgDownloaderTest.php +++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php @@ -98,7 +98,9 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(null)); $downloader = $this->getDownloaderMock(); + $downloader->prepare('update', $sourcePackageMock, '/path', $initialPackageMock); $downloader->update($initialPackageMock, $sourcePackageMock, '/path'); + $downloader->cleanup('update', $sourcePackageMock, '/path', $initialPackageMock); } public function testUpdate() @@ -129,7 +131,9 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testRemove() @@ -148,7 +152,9 @@ class HgDownloaderTest extends TestCase ->will($this->returnValue(true)); $downloader = $this->getDownloaderMock(null, null, $processExecutor, $filesystem); + $downloader->prepare('uninstall', $packageMock, 'composerPath'); $downloader->remove($packageMock, 'composerPath'); + $downloader->cleanup('uninstall', $packageMock, 'composerPath'); } public function testGetInstallationSource() diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index e69149271..63c407218 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -338,7 +338,7 @@ class ZipDownloaderTest extends TestCase class MockedZipDownloader extends ZipDownloader { - public function download(PackageInterface $package, $path, $output = true) + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { return; } From 607b487295dba0f6b6063d7fdba33f3b5aa6c863 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 29 Aug 2019 11:45:19 +0200 Subject: [PATCH 102/321] Fix missing use/undefined var --- src/Composer/Downloader/GitDownloader.php | 2 +- src/Composer/Downloader/VcsDownloader.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 300749ca8..3d3d947f4 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -134,7 +134,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $msg = "Checking out ".$this->getShortHash($ref); $command = 'git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer)'; if (getenv('COMPOSER_DISABLE_NETWORK')) { - throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting'); + throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting'); } } diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index 15bf55072..ce0a4bd9f 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -20,6 +20,7 @@ use Composer\Package\Version\VersionParser; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use Composer\Util\Filesystem; +use React\Promise\PromiseInterface; /** * @author Jordi Boggiano From f1e4ccbe1dd89e183bd3e063f0c049baa4d9818a Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 01:58:12 +0200 Subject: [PATCH 103/321] Fix handling of reference updates and root references --- .../LocalRepoTransaction.php | 5 +- .../DependencyResolver/LockTransaction.php | 11 +- .../DependencyResolver/PoolBuilder.php | 40 +++- src/Composer/DependencyResolver/Request.php | 7 +- src/Composer/Installer.php | 42 +--- src/Composer/Repository/RepositorySet.php | 7 +- .../Test/DependencyResolver/SolverTest.php | 2 +- .../installer/update-changes-url.test | 14 +- .../installer/update-mirrors-changes-url.test | 213 ++++++++++++++++++ 9 files changed, 280 insertions(+), 61 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index 98c0b05d7..4ac747243 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -62,9 +62,10 @@ class LocalRepoTransaction // do we need to update? if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { $operations[] = new Operation\UpdateOperation($source, $package); - } else { - // TODO do we need to update metadata? force update based on reference? + } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) { + $operations[] = new Operation\UpdateOperation($source, $package); } + unset($removeMap[$package->getName()]); } else { $operations[] = new Operation\InstallOperation($package); unset($removeMap[$package->getName()]); diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index ad25f8fc1..e113988bd 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -108,7 +108,7 @@ class LockTransaction } // TODO additionalFixedRepository needs to be looked at here as well? - public function getNewLockNonDevPackages(array $rootForceReferences) + public function getNewLockNonDevPackages() { $packages = array(); foreach ($this->decisions as $i => $decision) { @@ -117,14 +117,6 @@ class LockTransaction if ($literal > 0) { $package = $this->pool->literalToPackage($literal); if (!isset($this->unlockableMap[$package->id]) && !($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { - - echo "rootRef? ".$package->getName()."\n"; - // TODO can we really just do this for all of them here? What if we're doing a partial update, should this change anyway? - if (isset($rootForceReferences[$package->getName()])) { - echo "rootRef! ".$package->getName()."\n"; - $package->setSourceReference($rootForceReferences[$package->getName()]); - } - $packages[] = $package; } } @@ -135,6 +127,7 @@ class LockTransaction public function getNewLockDevPackages() { + // TODO this is empty? $packages = array(); return $packages; } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 8f164df71..f9a7541c3 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -14,11 +14,11 @@ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; +use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Repository\AsyncRepositoryInterface; use Composer\Repository\ComposerRepository; use Composer\Repository\InstalledRepositoryInterface; -use Composer\Repository\LockArrayRepository; use Composer\Repository\PlatformRepository; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MultiConstraint; @@ -31,6 +31,7 @@ class PoolBuilder private $isPackageAcceptableCallable; private $filterRequires; private $rootAliases; + private $rootReferences; private $aliasMap = array(); private $nameConstraints = array(); @@ -46,16 +47,17 @@ class PoolBuilder $this->filterRequires = $filterRequires; } - public function buildPool(array $repositories, array $rootAliases, Request $request) + public function buildPool(array $repositories, array $rootAliases, array $rootReferences, Request $request) { $pool = new Pool($this->filterRequires); $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; // TODO do we really want the request here? kind of want a root requirements thingy instead $loadNames = array(); foreach ($request->getFixedPackages() as $package) { // TODO can actually use very specific constraint - $loadNames[$package->getname()] = null; + $loadNames[$package->getName()] = null; } foreach ($request->getJobs() as $job) { switch ($job['cmd']) { @@ -94,7 +96,7 @@ class PoolBuilder foreach ($packages as $package) { if (call_user_func($this->isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { - $newLoadNames += $this->loadPackage($package, $key); + $newLoadNames += $this->loadPackage($request, $package, $key); } } } @@ -132,7 +134,7 @@ class PoolBuilder if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface) { foreach ($repository->getPackages() as $package) { - $this->loadPackage($package, $key); + $this->loadPackage($request, $package, $key); } } } @@ -146,7 +148,7 @@ class PoolBuilder return $pool; } - private function loadPackage(PackageInterface $package, $repoIndex) + private function loadPackage(Request $request, PackageInterface $package, $repoIndex) { $index = count($this->packages); $this->packages[] = $package; @@ -156,8 +158,18 @@ class PoolBuilder $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package; } - // handle root package aliases $name = $package->getName(); + + // we're simply setting the root references on all versions for a name here and rely on the solver to pick the + // 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->isFixedPackage($package)) { + $this->setReferences($package, $this->rootReferences[$name]); + } + } + if (isset($this->rootAliases[$name][$package->getVersion()])) { $alias = $this->rootAliases[$name][$package->getVersion()]; if ($package instanceof AliasPackage) { @@ -194,5 +206,19 @@ class PoolBuilder return $loadNames; } + + private function setReferences(Package $package, $reference) + { + $package->setSourceReference($reference); + + // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL + // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this? + if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { + $package->setDistReference($reference); + $package->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $package->getDistUrl())); + } elseif ($package->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it + $package->setDistReference($reference); + } + } } diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index a225e33b6..166ebbe07 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -52,7 +52,7 @@ class Request $package = $package->getAliasOf(); } - $this->fixedPackages[] = $package; + $this->fixedPackages[spl_object_hash($package)] = $package; if (!$lockable) { $this->unlockables[] = $package; @@ -80,6 +80,11 @@ class Request return $this->fixedPackages; } + public function isFixedPackage(PackageInterface $package) + { + return isset($this->fixedPackages[spl_object_hash($package)]); + } + public function getPresentMap() { $presentMap = array(); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index c07fc44c9..a59a4dc4c 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -39,6 +39,7 @@ use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Package\Package; use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\Package\Locker; @@ -375,25 +376,21 @@ class Installer // to the version specified in the lock if ($this->updateWhitelist) { foreach ($lockedRepository->getPackages() as $lockedPackage) { + // TODO should this really be checking acceptability here? if (!$this->isUpdateable($lockedPackage) && $repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { - // need to actually allow for metadata updates at all times, so we want to fix the most recent prefered package in the repo set instead - $packages = $repositorySet->findPackages($lockedPackage->getName(), new Constraint('=', $lockedPackage->getVersion())); - $lockedPackage = isset($packages[0]) ? $packages[0] : $lockedPackage; - - // in how far do we need to reset requirements here, theoretically it's the same version so nothing should have changed, but for a dev version it could have? - - // TODO add reason for fix? $request->fixPackage($lockedPackage); } } } + // TODO reenable events //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); $pool = $repositorySet->createPool($request); // TODO ensure that the solver always picks most recent reference for dev packages, so they get updated even when just a new commit is pushed but version is unchanged + // should already be solved by using the remote package in all cases in the pool // solve dependencies $solver = new Solver($policy, $pool, $this->io); @@ -426,7 +423,7 @@ class Installer $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockNonDevPackages($this->package->getReferences()), + $lockTransaction->getNewLockNonDevPackages(), $lockTransaction->getNewLockDevPackages(), $platformReqs, $platformDevReqs, @@ -504,6 +501,8 @@ class Installer } } + $this->io->write('foo'); + if ($doInstall) { // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false return $this->doInstall($localRepo, true); @@ -703,7 +702,7 @@ class Installer $this->fixedRootPackage->setRequires(array()); $this->fixedRootPackage->setDevRequires(array()); - $repositorySet = new RepositorySet($rootAliases, $minimumStability, $stabilityFlags, $rootConstraints); + $repositorySet = new RepositorySet($rootAliases, $this->package->getReferences(), $minimumStability, $stabilityFlags, $rootConstraints); $repositorySet->addRepository(new InstalledArrayRepository(array($this->fixedRootPackage))); $repositorySet->addRepository($platformRepo); if ($this->additionalFixedRepository) { @@ -793,28 +792,7 @@ class Installer return $normalizedAliases; } - private function updatePackageUrl(PackageInterface $package, $sourceUrl, $sourceType, $sourceReference, $distUrl) - { - $oldSourceRef = $package->getSourceReference(); - - if ($package->getSourceUrl() !== $sourceUrl) { - $package->setSourceType($sourceType); - $package->setSourceUrl($sourceUrl); - $package->setSourceReference($sourceReference); - } - - // only update dist url for github/bitbucket/gitlab dists as they use a combination of dist url + dist reference to install - // but for other urls this is ambiguous and could result in bad outcomes - if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $distUrl)) { - $package->setDistUrl($distUrl); - $this->updateInstallReferences($package, $sourceReference); - } - - if ($this->updateWhitelist && !$this->isUpdateable($package)) { - $this->updateInstallReferences($package, $oldSourceRef); - } - } - + // TODO do we still need this function? private function updateInstallReferences(PackageInterface $package, $reference) { if (!$reference) { @@ -910,7 +888,7 @@ class Installer } } - $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($lockRepo); $seen = array(); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 6a232630d..f7f8a57f8 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -29,6 +29,8 @@ class RepositorySet { /** @var array */ private $rootAliases; + /** @var array */ + private $rootReferences; /** @var RepositoryInterface[] */ private $repositories = array(); @@ -40,9 +42,10 @@ class RepositorySet /** @var Pool */ private $pool; - public function __construct(array $rootAliases = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) + public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) { $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; $this->acceptableStabilities = array(); foreach (BasePackage::$stabilities as $stability => $value) { @@ -151,7 +154,7 @@ class RepositorySet { $poolBuilder = new PoolBuilder(array($this, 'isPackageAcceptable'), $this->filterRequires); - return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $request); + return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $this->rootReferences, $request); } // TODO unify this with above in some simpler version without "request"? diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 76abd958a..3491f9c05 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -930,7 +930,7 @@ class SolverTest extends TestCase protected function createSolver() { - $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), $this->repoInstalled, new NullIO()); + $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), new NullIO()); } protected function checkSolverResult(array $expected) diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index ec108c7b9..c9df288f6 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -3,10 +3,10 @@ Update updates URLs for updated packages if they have changed a/a is dev and gets everything updated as it updates to a new ref b/b is a tag and gets everything updated by updating the package URL directly -c/c is a tag and not whitelisted and gets the new URL but keeps its old ref +c/c is a tag and not whitelisted and remains unchanged d/d is dev but with a #ref so it should get URL updated but not the reference e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref -e/e is dev but not whitelisted and gets the new URL but keeps its old ref +f/f is dev but not whitelisted and remains unchanged g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref --COMPOSER-- { @@ -161,8 +161,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, { "name": "c/c", "version": "1.0.0", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/newc", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/newc/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, { @@ -179,8 +179,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, { "name": "f/f", "version": "dev-master", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/newf/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, { @@ -208,6 +208,6 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an --RUN-- update a/a b/b d/d g/g --EXPECT-- -Installing e/e (dev-master 1111111) Updating a/a (dev-master 1111111) to a/a (dev-master 2222222) +Installing e/e (dev-master 1111111) Updating g/g (dev-master 0000000) to g/g (dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test new file mode 100644 index 000000000..9d88870b0 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test @@ -0,0 +1,213 @@ +--TEST-- +Update mirrors updates URLs for all packages if they have changed without updating versions + +a/a is dev and gets everything updated as it updates to a new ref +b/b is a tag and gets everything updated by updating the package URL directly +c/c is a tag and not whitelisted and gets the new URL but keeps its old ref +d/d is dev but with a #ref so it should get URL updated but not the reference +e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref +e/e is dev but not whitelisted and gets the new URL but keeps its old ref +g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/c/newc", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/c/newc/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/d/newd", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/d/newd/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "e/e", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/e/newe", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/e/newe/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/f/newf", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/f/newf/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/g/newg", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/g/newg/zipball/2222222222222222222222222222222222222222", "type": "zip" } + } + ] + } + ], + "require": { + "a/a": "dev-master", + "b/b": "2.0.3", + "c/c": "1.0.0", + "d/d": "dev-master#1111111111111111111111111111111111111111", + "e/e": "dev-master#1111111111111111111111111111111111111111", + "f/f": "dev-master", + "g/g": "dev-master#1111111111111111111111111111111111111111" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" } + } +] +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "type": "library" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/newc", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/newc/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/newd", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "e/e", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/e/newe", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/newf/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/g/newg", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "a/a": 20, + "d/d": 20, + "e/e": 20, + "f/f": 20, + "g/g": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update a/a b/b d/d g/g +--EXPECT-- +Installing e/e (dev-master 1111111) +Updating a/a (dev-master 1111111) to a/a (dev-master 2222222) +Updating g/g (dev-master 0000000) to g/g (dev-master 1111111) From 06d11f2f382335c082dfa8bd19c6dc21792f1083 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 02:28:40 +0200 Subject: [PATCH 104/321] Fix calculation of lock transaction updates and start updating output in tests --- .../LocalRepoTransaction.php | 5 +- .../DependencyResolver/LockTransaction.php | 2 +- src/Composer/DependencyResolver/Request.php | 2 +- src/Composer/Installer.php | 51 +++++++++---------- .../installer/broken-deps-do-not-replace.test | 2 +- .../installer/github-issues-4319.test | 2 +- .../installer/github-issues-4795-2.test | 28 ++++++++-- .../Test/Fixtures/installer/suggest-prod.test | 5 +- .../installer/suggest-uninstalled.test | 7 ++- ...dating-dev-from-lock-removes-old-deps.test | 2 +- 10 files changed, 67 insertions(+), 39 deletions(-) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index 4ac747243..db224989d 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -12,6 +12,8 @@ namespace Composer\DependencyResolver; +use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\Package\AliasPackage; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; @@ -86,7 +88,6 @@ class LocalRepoTransaction $operations = $this->movePluginsToFront($operations); $operations = $this->moveUninstallsToFront($operations); - // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? /* if ('update' === $jobType) { @@ -175,7 +176,7 @@ class LocalRepoTransaction { $uninstOps = array(); foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation) { + if ($op instanceof UninstallOperation || $op instanceof MarkAliasUninstalledOperation) { $uninstOps[] = $op; unset($operations[$idx]); } diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index e113988bd..63214be9d 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -78,8 +78,8 @@ class LockTransaction $operations[] = new Operation\UpdateOperation($lockMeansUpdateMap[abs($literal)], $package, $reason); // avoid updates to one package from multiple origins + $ignoreRemove[$lockMeansUpdateMap[abs($literal)]->id] = true; unset($lockMeansUpdateMap[abs($literal)]); - $ignoreRemove[$source->id] = true; } else { if ($package instanceof AliasPackage) { $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason); diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 166ebbe07..54adea9b5 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -90,7 +90,7 @@ class Request $presentMap = array(); if ($this->lockedRepository) { - foreach ($this->lockedRepository as $package) { + foreach ($this->lockedRepository->getPackages() as $package) { $presentMap[$package->id] = $package; } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index a59a4dc4c..791c7bcfd 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -351,7 +351,7 @@ class Installer } // TODO can we drop any locked packages that we have matching remote versions for? - $request = $this->createRequest($this->fixedRootPackage, $platformRepo); + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); if ($lockedRepository) { // TODO do we really always need this? Maybe only to skip fix() in updateWhitelist case cause these packages get removed on full update automatically? @@ -422,23 +422,6 @@ class Installer $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); - $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockNonDevPackages(), - $lockTransaction->getNewLockDevPackages(), - $platformReqs, - $platformDevReqs, - $aliases, - $this->package->getMinimumStability(), - $this->package->getStabilityFlags(), - $this->preferStable || $this->package->getPreferStable(), - $this->preferLowest, - $this->config->get('platform') ?: array(), - $this->writeLock && $this->executeOperations - ); - if ($updatedLock && $this->writeLock && $this->executeOperations) { - $this->io->writeError('Writing lock file'); - } - if ($lockTransaction->getOperations()) { $installs = $updates = $uninstalls = array(); foreach ($lockTransaction->getOperations() as $operation) { @@ -501,7 +484,22 @@ class Installer } } - $this->io->write('foo'); + $updatedLock = $this->locker->setLockData( + $lockTransaction->getNewLockNonDevPackages(), + $lockTransaction->getNewLockDevPackages(), + $platformReqs, + $platformDevReqs, + $aliases, + $this->package->getMinimumStability(), + $this->package->getStabilityFlags(), + $this->preferStable || $this->package->getPreferStable(), + $this->preferLowest, + $this->config->get('platform') ?: array(), + $this->writeLock && $this->executeOperations + ); + if ($updatedLock && $this->writeLock && $this->executeOperations) { + $this->io->writeError('Writing lock file'); + } if ($doInstall) { // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false @@ -530,7 +528,7 @@ class Installer $repositorySet = $this->createRepositorySet($platformRepo, $aliases, $lockedRepository); $repositorySet->addRepository($lockedRepository); - $this->io->writeError('Installing dependencies'.($this->devMode ? ' (including require-dev)' : '').' from lock file'); + $this->io->writeError('Installing dependencies from lock file'.($this->devMode ? ' (including require-dev)' : '').''); // verify that the lock file works with the current platform repository // we can skip this part if we're doing this as the second step after an update @@ -538,7 +536,7 @@ class Installer $this->io->writeError('Verifying lock file contents can be installed on current platform.'); // creating requirements request - $request = $this->createRequest($this->fixedRootPackage, $platformRepo); + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); if (!$this->locker->isFresh()) { $this->io->writeError('Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. Run update to update them.', true, IOInterface::QUIET); @@ -627,7 +625,7 @@ class Installer } // output op, but alias op only in debug verbosity - if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { + if ((!$this->executeOperations && false === strpos($operation->getJobType(), 'Alias')) || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation); } @@ -736,13 +734,14 @@ class Installer } /** - * @param RootPackageInterface $rootPackage - * @param PlatformRepository $platformRepo + * @param RootPackageInterface $rootPackage + * @param PlatformRepository $platformRepo + * @param RepositoryInterface|null $lockedRepository * @return Request */ - private function createRequest(RootPackageInterface $rootPackage, PlatformRepository $platformRepo) + private function createRequest(RootPackageInterface $rootPackage, PlatformRepository $platformRepo, $lockedRepository = null) { - $request = new Request(); + $request = new Request($lockedRepository); $request->fixPackage($rootPackage, false); diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index db4ef23c0..a4bfe6a4d 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -22,7 +22,7 @@ Broken dependencies should not lead to a replacer being installed which is not m install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index ee221dab0..b387942fb 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -32,7 +32,7 @@ install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index 877ac3653..878e9429a 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -29,15 +29,37 @@ that are also a root package, when that root package is also explicitly whitelis { "name": "a/a", "version": "1.0.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] - +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "1.0.0" + }, + { + "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update a/a b/b --with-dependencies --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 0 installs, 2 updates, 0 removals +Updating dependencies +Lock file operations: 0 installs, 2 updates, 0 removals + - Updating b/b (1.0.0) to b/b (1.1.0) + - Updating a/a (1.0.0) to a/a (1.1.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index 40546f8d0..e28ef03e5 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -19,8 +19,11 @@ install --no-dev --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies -Package operations: 1 install, 0 updates, 0 removals +Lock file operations: 1 install, 0 updates, 0 removals + - Installing a/a (1.0.0) Writing lock file +Installing dependencies from lock file +Package operations: 1 install, 0 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index ae5ff36e3..ab22eeb6e 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -18,10 +18,13 @@ Suggestions are displayed install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Installing a/a (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals a/a suggests installing b/b (an obscure reason) -Writing lock file Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index 04624561d..6575530d0 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -16,7 +16,7 @@ Installing locked dev packages should remove old dependencies "require": {} } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], From b700aa3d622895a79a7334b751b462c28a72b385 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 06:03:39 +0200 Subject: [PATCH 105/321] Sort local repo transaction as topological as possible --- .../LocalRepoTransaction.php | 100 +++++++++++++++++- tests/Composer/Test/InstallerTest.php | 2 +- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index db224989d..8a38248f7 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -72,8 +72,6 @@ class LocalRepoTransaction $operations[] = new Operation\InstallOperation($package); unset($removeMap[$package->getName()]); } - - /* if (isset($lockedPackages[$package->getName()])) { die("Alias?"); @@ -85,7 +83,10 @@ class LocalRepoTransaction $operations[] = new Operation\UninstallOperation($package, null); } + $operations = $this->sortOperations($operations); $operations = $this->movePluginsToFront($operations); + // TODO fix this: + // we have to do this again here even though sortOperations did it because moving plugins moves them before uninstalls $operations = $this->moveUninstallsToFront($operations); // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? @@ -109,6 +110,99 @@ class LocalRepoTransaction return $operations; } + // TODO is there a more efficient / better way to do get a "good" install order? + public function sortOperations(array $operations) + { + $packageQueue = $this->lockedRepository->getPackages(); + + $packageQueue[] = null; // null is a cycle marker + + $weights = array(); + $foundWeighables = false; + + // This is sort of a topological sort, the weight represents the distance from a leaf (1 == is leaf) + // Since we can have cycles in the dep graph, any node which doesn't have an acyclic connection to all + // leaves it's connected to, cannot be assigned a weight and will be unsorted + while (!empty($packageQueue)) { + $package = array_shift($packageQueue); + + // one full cycle + if ($package === null) { + // if we were able to assign some weights, keep going + if ($foundWeighables) { + $foundWeighables = false; + $packageQueue[] = null; + continue; + } else { + foreach ($packageQueue as $package) { + $weights[$package->getName()] = PHP_INT_MAX; + } + // no point in continuing, we are in a cycle + break; + } + } + + $requires = array_filter(array_keys($package->getRequires()), function ($req) { + return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); + + $maxWeight = 0; + foreach ($requires as $require) { + if (!isset($weights[$require])) { + $maxWeight = null; + + // needs more calculation, so add to end of queue + $packageQueue[] = $package; + break; + } + + $maxWeight = max((int) $maxWeight, $weights[$require]); + } + if ($maxWeight !== null) { + $foundWeighables = true; + $weights[$package->getName()] = $maxWeight + 1; + } + } + + // TODO do we have any alias ops in the local repo transaction? + usort($operations, function ($opA, $opB) use ($weights) { + // uninstalls come first, if there are multiple, sort by name + if ($opA instanceof Operation\UninstallOperation) { + $packageA = $opA->getPackage(); + if ($opB instanceof Operation\UninstallOperation) { + return strcmp($packageA->getName(), $opB->getPackage()->getName()); + } + return -1; + } elseif ($opB instanceof Operation\UninstallOperation) { + return 1; + } + + + if ($opA instanceof Operation\InstallOperation) { + $packageA = $opA->getPackage(); + } elseif ($opA instanceof Operation\UpdateOperation) { + $packageA = $opA->getTargetPackage(); + } + + if ($opB instanceof Operation\InstallOperation) { + $packageB = $opB->getPackage(); + } elseif ($opB instanceof Operation\UpdateOperation) { + $packageB = $opB->getTargetPackage(); + } + + $weightA = $weights[$packageA->getName()]; + $weightB = $weights[$packageB->getName()]; + + if ($weightA === $weightB) { + return strcmp($packageA->getName(), $packageB->getName()); + } else { + return $weightA < $weightB ? -1 : 1; + } + }); + + return $operations; + } + /** * Workaround: if your packages depend on plugins, we must be sure * that those are installed / updated first; else it would lead to packages @@ -176,7 +270,7 @@ class LocalRepoTransaction { $uninstOps = array(); foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation || $op instanceof MarkAliasUninstalledOperation) { + if ($op instanceof UninstallOperation) { $uninstOps[] = $op; unset($operations[$idx]); } diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 89385c288..ce87d111f 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -97,7 +97,7 @@ class InstallerTest extends TestCase })); $tempLockData = null; - $locker = new Locker($io, $lockJsonMock, $repositoryManager, $installationManager, '{}'); + $locker = new Locker($io, $lockJsonMock, $installationManager, '{}'); $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); From 4db325b7b463fa8053f05731878b0eab3d989ecb Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:13:34 +0200 Subject: [PATCH 106/321] Use the old root stack based approach to sorting operations in the transaction --- .../LocalRepoTransaction.php | 233 +++++++++--------- src/Composer/Installer.php | 6 + 2 files changed, 128 insertions(+), 111 deletions(-) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index 8a38248f7..c115c9e0c 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -15,28 +15,54 @@ namespace Composer\DependencyResolver; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\Package\AliasPackage; +use Composer\Package\Link; +use Composer\Package\PackageInterface; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; +use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann */ class LocalRepoTransaction { - /** @var RepositoryInterface */ - protected $lockedRepository; + /** @var array */ + protected $lockedPackages; + protected $lockedPackagesByName = array(); /** @var RepositoryInterface */ protected $localRepository; - public function __construct($lockedRepository, $localRepository) + public function __construct(RepositoryInterface $lockedRepository, $localRepository) { - $this->lockedRepository = $lockedRepository; $this->localRepository = $localRepository; - + $this->setLockedPackageMaps($lockedRepository); $this->operations = $this->calculateOperations(); } + private function setLockedPackageMaps($lockedRepository) + { + $packageSort = function (PackageInterface $a, PackageInterface $b) { + // sort alias packages by the same name behind their non alias version + if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) { + return $a instanceof AliasPackage ? -1 : 1; + } + return strcmp($b->getName(), $a->getName()); + }; + + foreach ($lockedRepository->getPackages() as $package) { + $this->lockedPackages[$package->id] = $package; + foreach ($package->getNames() as $name) { + $this->lockedPackagesByName[$name][] = $package; + } + } + + uasort($this->lockedPackages, $packageSort); + foreach ($this->lockedPackagesByName as $name => $packages) { + uasort($this->lockedPackagesByName[$name], $packageSort); + } + } + public function getOperations() { return $this->operations; @@ -48,45 +74,84 @@ class LocalRepoTransaction $localPackageMap = array(); $removeMap = array(); + $localAliasMap = array(); + $removeAliasMap = array(); foreach ($this->localRepository->getPackages() as $package) { - if (isset($localPackageMap[$package->getName()])) { - die("Alias?"); + if ($package instanceof AliasPackage) { + $localAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + } else { + $localPackageMap[$package->getName()] = $package; + $removeMap[$package->getName()] = $package; } - $localPackageMap[$package->getName()] = $package; - $removeMap[$package->getName()] = $package; } - $lockedPackages = array(); - foreach ($this->lockedRepository->getPackages() as $package) { - if (isset($localPackageMap[$package->getName()])) { - $source = $localPackageMap[$package->getName()]; + $stack = $this->getRootPackages(); - // do we need to update? - if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { - $operations[] = new Operation\UpdateOperation($source, $package); - } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) { - $operations[] = new Operation\UpdateOperation($source, $package); + $visited = array(); + $processed = array(); + + while (!empty($stack)) { + $package = array_pop($stack); + + if (isset($processed[$package->id])) { + continue; + } + + if (!isset($visited[$package->id])) { + $visited[$package->id] = true; + + $stack[] = $package; + if ($package instanceof AliasPackage) { + $stack[] = $package->getAliasOf(); + } else { + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->getLockedProviders($link); + + foreach ($possibleRequires as $require) { + $stack[] = $require; + } + } + } + } elseif (!isset($processed[$package->id])) { + $processed[$package->id] = true; + + if ($package instanceof AliasPackage) { + $aliasKey = $package->getName().'::'.$package->getVersion(); + if (isset($localAliasMap[$aliasKey])) { + unset($removeAliasMap[$aliasKey]); + } else { + $operations[] = new Operation\MarkAliasInstalledOperation($package); + } + } else { + if (isset($localPackageMap[$package->getName()])) { + $source = $localPackageMap[$package->getName()]; + + // do we need to update? + if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } + unset($removeMap[$package->getName()]); + } else { + $operations[] = new Operation\InstallOperation($package); + unset($removeMap[$package->getName()]); + } } - unset($removeMap[$package->getName()]); - } else { - $operations[] = new Operation\InstallOperation($package); - unset($removeMap[$package->getName()]); } -/* - if (isset($lockedPackages[$package->getName()])) { - die("Alias?"); - } - $lockedPackages[$package->getName()] = $package;*/ } foreach ($removeMap as $name => $package) { - $operations[] = new Operation\UninstallOperation($package, null); + array_unshift($operations, new Operation\UninstallOperation($package, null)); + } + foreach ($removeAliasMap as $nameVersion => $package) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package, null); } - $operations = $this->sortOperations($operations); $operations = $this->movePluginsToFront($operations); // TODO fix this: - // we have to do this again here even though sortOperations did it because moving plugins moves them before uninstalls + // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls $operations = $this->moveUninstallsToFront($operations); // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? @@ -110,97 +175,43 @@ class LocalRepoTransaction return $operations; } - // TODO is there a more efficient / better way to do get a "good" install order? - public function sortOperations(array $operations) + /** + * Determine which packages in the lock file are not required by any other packages in the lock file. + * + * These serve as a starting point to enumerate packages in a topological order despite potential cycles. + * If there are packages with a cycle on the top level the package with the lowest name gets picked + * + * @return array + */ + private function getRootPackages() { - $packageQueue = $this->lockedRepository->getPackages(); + $roots = $this->lockedPackages; - $packageQueue[] = null; // null is a cycle marker + foreach ($this->lockedPackages as $packageId => $package) { + if (!isset($roots[$packageId])) { + continue; + } - $weights = array(); - $foundWeighables = false; + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->getLockedProviders($link); - // This is sort of a topological sort, the weight represents the distance from a leaf (1 == is leaf) - // Since we can have cycles in the dep graph, any node which doesn't have an acyclic connection to all - // leaves it's connected to, cannot be assigned a weight and will be unsorted - while (!empty($packageQueue)) { - $package = array_shift($packageQueue); - - // one full cycle - if ($package === null) { - // if we were able to assign some weights, keep going - if ($foundWeighables) { - $foundWeighables = false; - $packageQueue[] = null; - continue; - } else { - foreach ($packageQueue as $package) { - $weights[$package->getName()] = PHP_INT_MAX; + foreach ($possibleRequires as $require) { + if ($require !== $package) { + unset($roots[$require->id]); } - // no point in continuing, we are in a cycle - break; } } - - $requires = array_filter(array_keys($package->getRequires()), function ($req) { - return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); - }); - - $maxWeight = 0; - foreach ($requires as $require) { - if (!isset($weights[$require])) { - $maxWeight = null; - - // needs more calculation, so add to end of queue - $packageQueue[] = $package; - break; - } - - $maxWeight = max((int) $maxWeight, $weights[$require]); - } - if ($maxWeight !== null) { - $foundWeighables = true; - $weights[$package->getName()] = $maxWeight + 1; - } } - // TODO do we have any alias ops in the local repo transaction? - usort($operations, function ($opA, $opB) use ($weights) { - // uninstalls come first, if there are multiple, sort by name - if ($opA instanceof Operation\UninstallOperation) { - $packageA = $opA->getPackage(); - if ($opB instanceof Operation\UninstallOperation) { - return strcmp($packageA->getName(), $opB->getPackage()->getName()); - } - return -1; - } elseif ($opB instanceof Operation\UninstallOperation) { - return 1; - } + return $roots; + } - - if ($opA instanceof Operation\InstallOperation) { - $packageA = $opA->getPackage(); - } elseif ($opA instanceof Operation\UpdateOperation) { - $packageA = $opA->getTargetPackage(); - } - - if ($opB instanceof Operation\InstallOperation) { - $packageB = $opB->getPackage(); - } elseif ($opB instanceof Operation\UpdateOperation) { - $packageB = $opB->getTargetPackage(); - } - - $weightA = $weights[$packageA->getName()]; - $weightB = $weights[$packageB->getName()]; - - if ($weightA === $weightB) { - return strcmp($packageA->getName(), $packageB->getName()); - } else { - return $weightA < $weightB ? -1 : 1; - } - }); - - return $operations; + private function getLockedProviders(Link $link) + { + if (!isset($this->lockedPackagesByName[$link->getTarget()])) { + return array(); + } + return $this->lockedPackagesByName[$link->getTarget()]; } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8c1e9a801..9f5de754b 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -575,6 +575,12 @@ class Installer // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); + } else { + // we still need to ensure all packages have an id for correct functionality + $id = 1; + foreach ($lockedRepository->getPackages() as $package) { + $package->id = $id++; + } } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? From c16ab6174e8e394e710df9e18d03f995fb7a2330 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:16:43 +0200 Subject: [PATCH 107/321] Fix tests: Alias install now always right after origin package, partial update requires lock file --- ...pdate-installs-from-lock-even-missing.test | 4 ++-- ...elist-patterns-with-root-dependencies.test | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test index 9a6f0ab9e..ea4a7c997 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test @@ -98,8 +98,8 @@ update b/b } --EXPECT-- Updating a/a (dev-master oldmaster-a) to a/a (dev-master newmaster-a) -Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2) Marking a/a (2.2.x-dev newmaster-a) as installed, alias of a/a (dev-master newmaster-a) +Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2) Marking b/b (2.3.x-dev newmaster-b2) as installed, alias of b/b (dev-master newmaster-b2) -Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a) +Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test index a24bafb91..fe0cb1e31 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test @@ -47,6 +47,28 @@ Update with a package whitelist only updates those packages and their dependenci { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, + { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, + { "name": "whitelisted-component4", "version": "1.0.0" }, + { "name": "whitelisted-component5", "version": "1.0.0" }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update whitelisted-* --with-dependencies --EXPECT-- From 3e0e5dc1fab534fd0e139208faea32b4a4e4660a Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:17:41 +0200 Subject: [PATCH 108/321] Fix test expectation: Install and update operations are now alphabetical --- .../Composer/Test/Fixtures/installer/circular-dependency2.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/circular-dependency2.test b/tests/Composer/Test/Fixtures/installer/circular-dependency2.test index c89beef6b..6024c17f2 100644 --- a/tests/Composer/Test/Fixtures/installer/circular-dependency2.test +++ b/tests/Composer/Test/Fixtures/installer/circular-dependency2.test @@ -32,5 +32,5 @@ Circular dependencies are possible between packages --RUN-- update -v --EXPECT-- -Installing require/itself (1.0.0) Installing regular/pkg (1.0.0) +Installing require/itself (1.0.0) From a114e26841b5bec32576c7e42cbaa7c1de7040a6 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:43:23 +0200 Subject: [PATCH 109/321] Correctly load aliases in lockedRepository to fix alias install output --- src/Composer/Package/Locker.php | 13 ++++++- .../aliased-priority-conflicting.test | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 7aad4d318..d790837a5 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -171,8 +171,19 @@ class Locker } if (isset($lockedPackages[0]['name'])) { + $packageByName = array(); foreach ($lockedPackages as $info) { - $packages->addPackage($this->loader->load($info)); + $package = $this->loader->load($info); + $packages->addPackage($package); + $packageByName[$package->getName()] = $package; + } + + if (isset($lockData['aliases'])) { + foreach ($lockData['aliases'] as $alias) { + if (isset($packageByName[$alias['package']])) { + $packages->addPackage(new AliasPackage($packageByName[$alias['package']], $alias['alias_normalized'], $alias['alias'])); + } + } } return $packages; diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test index cda5d31d3..8d928c20d 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test @@ -43,6 +43,45 @@ Aliases take precedence over default package even if default is selected }, "minimum-stability": "dev" } +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "require": { "a/req": "dev-master" }, + "type": "library" + }, + { + "name": "a/b", "version": "dev-master", + "require": { "a/req": "dev-master" }, + "type": "library" + }, + { + "name": "a/req", "version": "dev-feature-foo", + "source": { "reference": "feat.f", "type": "git", "url": "" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [ + { + "alias": "dev-master", + "alias_normalized": "9999999-dev", + "version": "dev-feature-foo", + "package": "a/req" + } + ], + "minimum-stability": "dev", + "stability-flags": { + "a/a": 20, + "a/b": 20, + "a/req": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- install --EXPECT-- From 33ff67abf3fda8d870208fff2ef4cf43698599db Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:53:31 +0200 Subject: [PATCH 110/321] Update tests for new output and for required lock file on partial update --- .../Fixtures/installer/aliased-priority.test | 2 +- .../Fixtures/installer/suggest-installed.test | 8 ++++++-- .../update-whitelist-locked-require.test | 17 +++++++++++++++++ ...telist-patterns-with-all-dependencies.test | 19 +++++++++++++++++++ .../update-whitelist-removes-unused.test | 16 ++++++++++++++++ .../Fixtures/installer/update-whitelist.test | 18 ++++++++++++++++++ 6 files changed, 77 insertions(+), 3 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority.test b/tests/Composer/Test/Fixtures/installer/aliased-priority.test index 97ffe5521..8dd0e8470 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority.test @@ -51,6 +51,6 @@ install Installing a/c (dev-feature-foo feat.f) Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f) Installing a/b (dev-master forked) +Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) Installing a/a (dev-master master) Marking a/a (1.0.x-dev master) as installed, alias of a/a (dev-master master) -Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index 198203ce9..468a53612 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -20,9 +20,13 @@ Suggestions are not displayed for installed packages install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 2 installs, 0 updates, 0 removals +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Installing b/b (1.0.0) + - Installing a/a (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test index 381416af1..c3b20426e 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test @@ -29,6 +29,23 @@ Update with a package whitelist only updates those packages if they are not pres { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, { "name": "fixed-sub-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, + { "name": "fixed-sub-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update whitelisted dependency --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test index 8ea177cad..b3396b376 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test @@ -38,6 +38,25 @@ Update with a package whitelist pattern and all-dependencies flag updates packag { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted-component1", "version": "1.0.0" }, + { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update whitelisted-* --with-all-dependencies --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test index e658e8c06..3b28e4671 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -25,6 +25,22 @@ Update with a package whitelist removes unused packages { "name": "fixed-dependency", "version": "1.0.0" }, { "name": "old-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, + { "name": "fixed-dependency", "version": "1.0.0" }, + { "name": "old-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update --with-dependencies whitelisted --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index 751d79e70..b53954dc6 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -33,6 +33,24 @@ Update with a package whitelist only updates those packages listed as command ar { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update whitelisted --EXPECT-- From 3989a1b8ee387b4bc38df74933f90c5e211d8cb7 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 08:52:10 +0200 Subject: [PATCH 111/321] Restore dev package extraction New approach is to use only the solved set of packages as input and then to resolve with only the non-dev requirements and to mark everything as dev that is not part of the result set, rather than transitioning a temporary local repo state by uninstalling dev packages. --- .../LocalRepoTransaction.php | 6 + .../DependencyResolver/LockTransaction.php | 46 ++++-- src/Composer/Installer.php | 133 ++++++++++++++++-- .../update-no-dev-still-resolves-dev.test | 2 +- 4 files changed, 168 insertions(+), 19 deletions(-) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index c115c9e0c..a41798b50 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -33,6 +33,9 @@ class LocalRepoTransaction /** @var RepositoryInterface */ protected $localRepository; + /** + * Reassigns ids for all packages in the lockedrepository + */ public function __construct(RepositoryInterface $lockedRepository, $localRepository) { $this->localRepository = $localRepository; @@ -50,7 +53,10 @@ class LocalRepoTransaction return strcmp($b->getName(), $a->getName()); }; + $id = 1; + $this->lockedPackages = array(); foreach ($lockedRepository->getPackages() as $package) { + $package->id = $id++; $this->lockedPackages[$package->id] = $package; foreach ($package->getNames() as $name) { $this->lockedPackagesByName[$name][] = $package; diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 63214be9d..3d5204067 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -16,7 +16,9 @@ use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Package\AliasPackage; use Composer\Package\RootAliasPackage; use Composer\Package\RootPackageInterface; +use Composer\Repository\ArrayRepository; use Composer\Repository\RepositoryInterface; +use Composer\Test\Repository\ArrayRepositoryTest; /** * @author Nils Adermann @@ -40,7 +42,8 @@ class LockTransaction protected $unlockableMap; protected $decisions; - protected $transaction; + + protected $resultPackages; public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions) { @@ -104,31 +107,54 @@ class LockTransaction } } + $this->setResultPackages(); + return $operations; } - // TODO additionalFixedRepository needs to be looked at here as well? - public function getNewLockNonDevPackages() + // TODO make this a bit prettier instead of the two text indexes? + public function setResultPackages() { - $packages = array(); + $this->resultPackages = array('non-dev' => array(), 'dev' => array()); foreach ($this->decisions as $i => $decision) { $literal = $decision[Decisions::DECISION_LITERAL]; if ($literal > 0) { $package = $this->pool->literalToPackage($literal); - if (!isset($this->unlockableMap[$package->id]) && !($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { - $packages[] = $package; + if (!isset($this->unlockableMap[$package->id])) { + $this->resultPackages['non-dev'][] = $package; } } } - - return $packages; } - public function getNewLockDevPackages() + public function setNonDevPackages(LockTransaction $extractionResult) + { + $packages = $extractionResult->getNewLockPackages(false); + + $this->resultPackages['dev'] = $this->resultPackages['non-dev']; + $this->resultPackages['non-dev'] = array(); + + foreach ($packages as $package) { + foreach ($this->resultPackages['dev'] as $i => $resultPackage) { + if ($package->getName() == $resultPackage->getName()) { + $this->resultPackages['non-dev'][] = $resultPackage; + unset($this->resultPackages['dev'][$i]); + } + } + } + } + + // TODO additionalFixedRepository needs to be looked at here as well? + public function getNewLockPackages($devMode) { - // TODO this is empty? $packages = array(); + foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { + if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + $packages[] = $package; + } + } + return $packages; } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9f5de754b..6c94df10e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -15,6 +15,7 @@ namespace Composer; use Composer\Autoload\AutoloadGenerator; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\LocalRepoTransaction; +use Composer\DependencyResolver\LockTransaction; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -40,6 +41,7 @@ use Composer\Package\Link; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Package; +use Composer\Repository\ArrayRepository; use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\Package\Locker; @@ -418,6 +420,8 @@ class Installer $this->io->writeError('Nothing to modify in lock file'); } + $this->extractDevPackages($lockTransaction, $platformRepo, $aliases, $policy); + // write lock $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); @@ -485,8 +489,8 @@ class Installer } $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockNonDevPackages(), - $lockTransaction->getNewLockDevPackages(), + $lockTransaction->getNewLockPackages(false), + $lockTransaction->getNewLockPackages(true), $platformReqs, $platformDevReqs, $aliases, @@ -509,6 +513,124 @@ class Installer return 0; } + /** + * Run the solver a second time on top of the existing update result with only the current result set in the pool + * and see what packages would get removed if we only had the non-dev packages in the solver request + */ + protected function extractDevPackages(LockTransaction $lockTransaction, $platformRepo, $aliases, $policy) + { + if (!$this->package->getDevRequires()) { + return array(); + } + + ; + + $resultRepo = new ArrayRepository(array()); + $loader = new ArrayLoader(null, true); + $dumper = new ArrayDumper(); + foreach ($lockTransaction->getNewLockPackages(false) as $pkg) { + $resultRepo->addPackage($loader->load($dumper->dump($pkg))); + } + + $repositorySet = $this->createRepositorySet($platformRepo, $aliases, null); + $repositorySet->addRepository($resultRepo); + + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, null); + + $links = $this->package->getRequires(); + foreach ($links as $link) { + $request->install($link->getTarget(), $link->getConstraint()); + } + + $pool = $repositorySet->createPool($request); + + // solve dependencies + $solver = new Solver($policy, $pool, $this->io); + try { + $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + $solver = null; + } catch (SolverProblemsException $e) { + // TODO change info message here + $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError($e->getMessage()); + + return max(1, $e->getCode()); + } + + $lockTransaction->setNonDevPackages($nonDevLockTransaction); + } + + + // TODO add proper output and events to above function based on old version below + /** + * Extracts the dev packages out of the localRepo + * + * This works by faking the operations so we can see what the dev packages + * would be at the end of the operation execution. This lets us then remove + * the dev packages from the list of operations accordingly if we are in a + * --no-dev install or update. + * + * @return array + private function extractDevPackages(array $operations, RepositoryInterface $localRepo, PlatformRepository $platformRepo, array $aliases) + { + // fake-apply all operations to this clone of the local repo so we see the complete set of package we would end up with + $tempLocalRepo = clone $localRepo; + foreach ($operations as $operation) { + switch ($operation->getJobType()) { + case 'install': + case 'markAliasInstalled': + if (!$tempLocalRepo->hasPackage($operation->getPackage())) { + $tempLocalRepo->addPackage(clone $operation->getPackage()); + } + break; + case 'uninstall': + case 'markAliasUninstalled': + $tempLocalRepo->removePackage($operation->getPackage()); + break; + case 'update': + $tempLocalRepo->removePackage($operation->getInitialPackage()); + if (!$tempLocalRepo->hasPackage($operation->getTargetPackage())) { + $tempLocalRepo->addPackage(clone $operation->getTargetPackage()); + } + break; + default: + throw new \LogicException('Unknown type: '.$operation->getJobType()); + } + } + // we have to reload the local repo to handle aliases properly + // but as it is not persisted on disk we use a loader/dumper + // to reload it in memory + $localRepo = new InstalledArrayRepository(array()); + $loader = new ArrayLoader(null, true); + $dumper = new ArrayDumper(); + foreach ($tempLocalRepo->getCanonicalPackages() as $pkg) { + $localRepo->addPackage($loader->load($dumper->dump($pkg))); + } + unset($tempLocalRepo, $loader, $dumper); + $policy = $this->createPolicy(); + $pool = $this->createPool(); + $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); + $pool->addRepository($installedRepo, $aliases); + // creating requirements request without dev requirements + $request = $this->createRequest($this->package, $platformRepo); + $request->updateAll(); + foreach ($this->package->getRequires() as $link) { + $request->install($link->getTarget(), $link->getConstraint()); + } + // solve deps to see which get removed + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); + $solver = new Solver($policy, $pool, $installedRepo, $this->io); + $ops = $solver->solve($request, $this->ignorePlatformReqs); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); + $devPackages = array(); + foreach ($ops as $op) { + if ($op->getJobType() === 'uninstall') { + $devPackages[] = $op->getPackage(); + } + } + return $devPackages; + }*/ + /** * @param RepositoryInterface $localRepo * @param RepositoryInterface $installedRepo @@ -575,12 +697,6 @@ class Installer // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); - } else { - // we still need to ensure all packages have an id for correct functionality - $id = 1; - foreach ($lockedRepository->getPackages() as $package) { - $package->id = $id++; - } } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? @@ -672,6 +788,7 @@ class Installer // TODO what's the point of rootConstraints at all, we generate the package pool taking them into account anyway? // TODO maybe we can drop the lockedRepository here + // TODO if this gets called in doInstall, this->update is still true?! if ($this->update) { $minimumStability = $this->package->getMinimumStability(); $stabilityFlags = $this->package->getStabilityFlags(); diff --git a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test index 3b90755e2..fad73f20a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test @@ -62,7 +62,7 @@ update --no-dev --EXPECT-- Uninstalling a/b (1.0.0) Updating a/a (1.0.0) to a/a (1.0.1) -Updating dev/pkg (dev-master old) to dev/pkg (dev-master new) Installing a/c (1.0.0) +Updating dev/pkg (dev-master old) to dev/pkg (dev-master new) Marking dev/pkg (1.1.x-dev new) as installed, alias of dev/pkg (dev-master new) Marking dev/pkg (1.0.x-dev old) as uninstalled, alias of dev/pkg (dev-master old) From 995b4f923e2cf6077abdef1999976e92bb30ac0b Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 09:18:54 +0200 Subject: [PATCH 112/321] Fix more tests which were lacking lock files for partial updates, display fix jobs in problems --- .../DependencyResolver/LockTransaction.php | 1 + src/Composer/DependencyResolver/Problem.php | 9 ++++++- .../DependencyResolver/RuleSetGenerator.php | 8 ++++++- .../install-from-lock-removes-package.test | 2 +- .../partial-update-without-lock.test | 17 +++++++++++++ .../Fixtures/installer/solver-problems.test | 17 ++++++++++++- .../update-with-all-dependencies.test | 24 ++++++++++++++++--- 7 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 3d5204067..a0aa4cd7e 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -137,6 +137,7 @@ class LockTransaction foreach ($packages as $package) { foreach ($this->resultPackages['dev'] as $i => $resultPackage) { + // TODO this comparison is probably insufficient, aliases, what about modified versions? I guess they aren't possible? if ($package->getName() == $resultPackage->getName()) { $this->resultPackages['non-dev'][] = $resultPackage; unset($this->resultPackages['dev'][$i]); diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index e07d4d0c1..2c1c7236a 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -90,7 +90,7 @@ class Problem $packages = array(); } - if ($job && $job['cmd'] === 'install' && empty($packages)) { + if ($job && ($job['cmd'] === 'install' || $job['cmd'] === 'fix') && empty($packages)) { // handle php/hhvm if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { @@ -208,6 +208,13 @@ class Problem $packageName = $job['packageName']; $constraint = $job['constraint']; switch ($job['cmd']) { + case 'fix': + $package = $job['package']; + if ($job['lockable']) { + return 'Package '.$package->getPrettyName().' is locked to version '.$package->getPrettyVersion(); + } else { + return 'Package '.$package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer'; + } case 'install': $packages = $this->pool->whatProvides($packageName, $constraint); if (!$packages) { diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 64cb27b11..0c7350cdd 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -12,9 +12,11 @@ namespace Composer\DependencyResolver; +use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Repository\PlatformRepository; +use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann @@ -254,8 +256,10 @@ class RuleSetGenerator return $impossible; } - protected function addRulesForRequest($request, $ignorePlatformReqs) + protected function addRulesForRequest(Request $request, $ignorePlatformReqs) { + $unlockableMap = $request->getUnlockableMap(); + foreach ($request->getFixedPackages() as $package) { $this->addRulesForPackage($package, $ignorePlatformReqs); @@ -263,6 +267,8 @@ class RuleSetGenerator 'cmd' => 'fix', 'packageName' => $package->getName(), 'constraint' => null, + 'package' => $package, + 'lockable' => !isset($unlockableMap[$package->id]), 'fixed' => true )); $this->addRule(RuleSet::TYPE_JOB, $rule); diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index 6063abfee..83569bfe7 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -25,7 +25,7 @@ Install from a lock file that deleted a package { "name": "whitelisted", "version": "1.1.0" }, { "name": "fixed-dependency", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test index e931a1f7d..845005649 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -28,6 +28,23 @@ Partial update without lock file should update everything whitelisted, remove ov { "name": "c/uptodate", "version": "1.0.0" }, { "name": "d/removed", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "a/old", "version": "1.0.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "1.0.0" }, + { "name": "d/removed", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update b/unstable --EXPECT-LOCK-- diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 8b1336db2..b6d2180db 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -30,6 +30,21 @@ Test the error output of solver problems. { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} + --RUN-- update unstable/package requirer/pkg dependency/pkg @@ -38,7 +53,7 @@ update unstable/package requirer/pkg dependency/pkg --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index c0019e6ca..14d8e13d8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -28,15 +28,33 @@ When `--with-all-dependencies` is used, Composer\Installer::whitelistUpdateDepen { "name": "a/a", "version": "1.0.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] - +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update b/b --with-all-dependencies --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 0 installs, 2 updates, 0 removals +Updating dependencies +Lock file operations: 0 installs, 2 updates, 0 removals + - Updating b/b (1.0.0) to b/b (1.1.0) + - Updating a/a (1.0.0) to a/a (1.1.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- From 4481cc4a8859504b908f2b9a8ece648e5423ffa8 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 29 Oct 2019 22:28:36 +0100 Subject: [PATCH 113/321] Allow an install request for a package name which is already fixed Ensures packages get loaded from locked repo correctly. We may not want to support this particular use-case at all, but for now it fixes the existing test, so we may want to revisit this later. --- src/Composer/DependencyResolver/PoolBuilder.php | 9 +++++++-- src/Composer/DependencyResolver/Problem.php | 6 +++--- src/Composer/DependencyResolver/RuleSetGenerator.php | 4 ++++ src/Composer/DependencyResolver/Solver.php | 1 - src/Composer/Installer.php | 2 -- .../Test/Fixtures/installer/solver-problems.test | 8 ++++---- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index f9a7541c3..5376367e2 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -62,8 +62,12 @@ class PoolBuilder foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': - $loadNames[$job['packageName']] = $job['constraint']; - $this->nameConstraints[$job['packageName']] = $job['constraint'] ? new MultiConstraint(array($job['constraint']), false) : null; + // TODO currently lock above is always NULL if we adjust that, this needs to merge constraints + // TODO does it really make sense that we can have install requests for the same package that is actively locked with non-matching constraints? + // also see the solver-problems.test test case + $constraint = array_key_exists($job['packageName'], $loadNames) ? null : $job['constraint']; + $loadNames[$job['packageName']] = $constraint; + $this->nameConstraints[$job['packageName']] = $constraint ? new MultiConstraint(array($job['constraint']), false) : null; break; } } @@ -199,6 +203,7 @@ class PoolBuilder // TODO addConstraint function? $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($linkConstraint), $this->nameConstraints[$require]->getConstraints()), false); } + // else it is null and should stay null } else { $this->nameConstraints[$require] = null; } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 2c1c7236a..bc3f2de04 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -211,10 +211,10 @@ class Problem case 'fix': $package = $job['package']; if ($job['lockable']) { - return 'Package '.$package->getPrettyName().' is locked to version '.$package->getPrettyVersion(); - } else { - return 'Package '.$package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer'; + 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'; case 'install': $packages = $this->pool->whatProvides($packageName, $constraint); if (!$packages) { diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 0c7350cdd..d42617a7a 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -261,6 +261,10 @@ class RuleSetGenerator $unlockableMap = $request->getUnlockableMap(); foreach ($request->getFixedPackages() as $package) { + if ($package->id == -1) { + throw new \RuntimeException("Fixed package ".$package->getName()." was not added to solver pool."); + } + $this->addRulesForPackage($package, $ignorePlatformReqs); $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_JOB_INSTALL, array( diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 348b17c2b..2abcf5d8a 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -147,7 +147,6 @@ class Solver if (abs($literal) !== abs($assertRuleLiteral)) { continue; } - $problem->addRule($assertRule); $this->disableProblem($assertRule); } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 6c94df10e..dea1e03e5 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -523,8 +523,6 @@ class Installer return array(); } - ; - $resultRepo = new ArrayRepository(array()); $loader = new ArrayLoader(null, true); $dumper = new ArrayDumper(); diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index b6d2180db..57d3ced92 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -61,12 +61,12 @@ Your requirements could not be resolved to an installable set of packages. Problem 2 - The requested package bogus/pkg could not be found in any version, there may be a typo in the package name. Problem 3 - - The requested package stable-requiree-excluded/pkg 1.0.1 exists as stable-requiree-excluded/pkg[1.0.0] but these are rejected by your constraint. - Problem 4 - - The requested package stable-requiree-excluded/pkg (installed at 1.0.0, required as 1.0.1) is satisfiable by stable-requiree-excluded/pkg[1.0.0] but these conflict with your requirements or minimum-stability. - Problem 5 - Installation request for requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. + Problem 4 + - stable-requiree-excluded/pkg is locked to version 1.0.0 and an update of this package was not requested. + - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.0, 1.0.1]. + - Installation request for stable-requiree-excluded/pkg 1.0.1 -> satisfiable by stable-requiree-excluded/pkg[1.0.1]. Potential causes: - A typo in the package name From 5c129be5e730e29522bc942b83fc200600c9be90 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 29 Oct 2019 23:12:54 +0100 Subject: [PATCH 114/321] Partial updates without a lock file are no longer possible, update test --- .../partial-update-without-lock.test | 41 +++---------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test index 845005649..74007af7b 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -1,5 +1,5 @@ --TEST-- -Partial update without lock file should update everything whitelisted, remove overly unstable packages +Partial update without lock file should error --COMPOSER-- { "repositories": [ @@ -28,41 +28,10 @@ Partial update without lock file should update everything whitelisted, remove ov { "name": "c/uptodate", "version": "1.0.0" }, { "name": "d/removed", "version": "1.0.0" } ] ---LOCK-- -{ - "packages": [ - { "name": "a/old", "version": "1.0.0" }, - { "name": "b/unstable", "version": "1.1.0-alpha" }, - { "name": "c/uptodate", "version": "1.0.0" }, - { "name": "d/removed", "version": "1.0.0" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} --RUN-- update b/unstable ---EXPECT-LOCK-- -{ - "packages": [ - { "name": "a/old", "version": "1.0.0", "type": "library" }, - { "name": "b/unstable", "version": "1.0.0", "type": "library" }, - { "name": "c/uptodate", "version": "1.0.0", "type": "library" }, - { "name": "d/removed", "version": "1.0.0", "type": "library" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} +--EXPECT-OUTPUT-- +Cannot update only a partial set of packages without a lock file present. +--EXPECT-EXIT-CODE-- +1 --EXPECT-- -Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) From 48ae45e5fe8da00aaf4aef5ab9bf9eacccfe5561 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 29 Oct 2019 23:16:38 +0100 Subject: [PATCH 115/321] Correct github issue test to include a lock file, still fails because of real bug now --- .../Fixtures/installer/github-issues-4795.test | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index 1f4b1af27..4afac72aa 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -30,13 +30,28 @@ dependency of one the requirements that is whitelisted for update. { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update b/b --with-dependencies --EXPECT-OUTPUT-- Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Nothing to install or update Writing lock file Generating autoload files From 0ff07015a1c0951be489ab46a66c093c714fd14c Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 30 Oct 2019 00:24:25 +0100 Subject: [PATCH 116/321] Only load package info from lock file for fixed packages As a result some lock file packages are no longer in the pool, so the former installed map, now present map cannot use package ids anymore Need to revisit some more code later to simplify this, todo notes left --- .../DependencyResolver/LockTransaction.php | 47 +++++++++++++------ .../DependencyResolver/PoolBuilder.php | 18 ++++++- src/Composer/DependencyResolver/Problem.php | 2 +- src/Composer/DependencyResolver/Request.php | 11 +++-- src/Composer/DependencyResolver/Solver.php | 2 +- .../installer/github-issues-4795.test | 4 +- .../Fixtures/installer/solver-problems.test | 2 +- 7 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index a0aa4cd7e..75b2efb5b 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -76,9 +76,14 @@ class LockTransaction $package = $this->pool->literalToPackage($literal); // wanted & !present - if ($literal > 0 && !isset($this->presentMap[$package->id])) { + if ($literal > 0 && !isset($this->presentMap[spl_object_hash($package)])) { if (isset($lockMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { - $operations[] = new Operation\UpdateOperation($lockMeansUpdateMap[abs($literal)], $package, $reason); + // TODO we end up here sometimes because we prefer the remote package now to get up to date metadata + // TODO define some level of identity here for what constitutes an update and what can be ignored? new kind of metadata only update? + $target = $lockMeansUpdateMap[abs($literal)]; + if ($package->getName() !== $target->getName() || $package->getVersion() !== $target->getVersion()) { + $operations[] = new Operation\UpdateOperation($target, $package, $reason); + } // avoid updates to one package from multiple origins $ignoreRemove[$lockMeansUpdateMap[abs($literal)]->id] = true; @@ -98,7 +103,7 @@ class LockTransaction $reason = $decision[Decisions::DECISION_REASON]; $package = $this->pool->literalToPackage($literal); - if ($literal <= 0 && isset($this->presentMap[$package->id]) && !isset($ignoreRemove[$package->id])) { + if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[$package->id])) { if ($package instanceof AliasPackage) { $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason); } else { @@ -163,29 +168,41 @@ class LockTransaction { $lockMeansUpdateMap = array(); + $packages = array(); + foreach ($this->decisions as $i => $decision) { $literal = $decision[Decisions::DECISION_LITERAL]; $package = $this->pool->literalToPackage($literal); + if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)])) { + $packages[spl_object_hash($package)] = $package; + } + } + + // some locked packages are not in the pool and thus, were not decided at all + foreach ($this->presentMap as $package) { + if ($package->id === -1) { + $packages[spl_object_hash($package)] = $package; + } + } + + foreach ($packages as $package) { if ($package instanceof AliasPackage) { continue; } - // !wanted & present - if ($literal <= 0 && isset($this->presentMap[$package->id])) { - // TODO can't we just look at existing rules? - $updates = $this->policy->findUpdatePackages($this->pool, $package); + // TODO can't we just look at existing rules? + $updates = $this->policy->findUpdatePackages($this->pool, $package); - $literals = array($package->id); + $literals = array($package->id); - foreach ($updates as $update) { - $literals[] = $update->id; - } + foreach ($updates as $update) { + $literals[] = $update->id; + } - foreach ($literals as $updateLiteral) { - if ($updateLiteral !== $literal && !isset($lockMeansUpdateMap[$updateLiteral])) { - $lockMeansUpdateMap[$updateLiteral] = $package; - } + foreach ($literals as $updateLiteral) { + if (!isset($lockMeansUpdateMap[$updateLiteral])) { + $lockMeansUpdateMap[$updateLiteral] = $package; } } } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 5376367e2..39ac5098f 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -59,6 +59,7 @@ class PoolBuilder // TODO can actually use very specific constraint $loadNames[$package->getName()] = null; } + foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': @@ -67,11 +68,24 @@ class PoolBuilder // also see the solver-problems.test test case $constraint = array_key_exists($job['packageName'], $loadNames) ? null : $job['constraint']; $loadNames[$job['packageName']] = $constraint; - $this->nameConstraints[$job['packageName']] = $constraint ? new MultiConstraint(array($job['constraint']), false) : null; + $this->nameConstraints[$job['packageName']] = $constraint ? new MultiConstraint(array($constraint), false) : null; break; } } + // packages from the locked repository only get loaded if they are explicitly fixed + foreach ($repositories as $key => $repository) { + if ($repository === $request->getLockedRepository()) { + foreach ($repository->getPackages() as $lockedPackage) { + foreach ($request->getFixedPackages() as $package) { + if ($package === $lockedPackage) { + $loadNames += $this->loadPackage($request, $package, $key); + } + } + } + } + } + while (!empty($loadNames)) { $loadIds = array(); foreach ($repositories as $key => $repository) { @@ -86,7 +100,7 @@ class PoolBuilder $newLoadNames = array(); foreach ($repositories as $key => $repository) { - if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface) { + if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface || $repository === $request->getLockedRepository()) { continue; } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index bc3f2de04..c2c3f42c6 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -68,7 +68,7 @@ class Problem /** * A human readable textual representation of the problem's reasons * - * @param array $installedMap A map of all installed packages + * @param array $installedMap A map of all present packages * @return string */ public function getPrettyString(array $installedMap = array(), array $learnedPool = array()) diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 54adea9b5..0656cbbae 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -85,18 +85,20 @@ class Request return isset($this->fixedPackages[spl_object_hash($package)]); } - public function getPresentMap() + // 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) { $presentMap = array(); if ($this->lockedRepository) { foreach ($this->lockedRepository->getPackages() as $package) { - $presentMap[$package->id] = $package; + $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package; } } foreach ($this->fixedPackages as $package) { - $presentMap[$package->id] = $package; + $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package; } return $presentMap; @@ -113,7 +115,8 @@ class Request return $unlockableMap; } - public function getLockMap() + public function getLockedRepository() { + return $this->lockedRepository; } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 2abcf5d8a..66f325446 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -218,7 +218,7 @@ class Solver $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); if ($this->problems) { - throw new SolverProblemsException($this->problems, $request->getPresentMap(), $this->learnedPool); + throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool); } return new LockTransaction($this->policy, $this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index 4afac72aa..8e5d17dfe 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -52,8 +52,10 @@ update b/b --with-dependencies Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. Loading composer repositories with package information Updating dependencies -Nothing to install or update +Nothing to modify in lock file Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 57d3ced92..94a0e3aa3 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -65,7 +65,7 @@ Your requirements could not be resolved to an installable set of packages. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. Problem 4 - stable-requiree-excluded/pkg is locked to version 1.0.0 and an update of this package was not requested. - - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.0, 1.0.1]. + - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.1, 1.0.0]. - Installation request for stable-requiree-excluded/pkg 1.0.1 -> satisfiable by stable-requiree-excluded/pkg[1.0.1]. Potential causes: From 94d45a980ccd30039fe405a5c3d179d86c5c08fb Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 30 Oct 2019 00:32:23 +0100 Subject: [PATCH 117/321] Update lock syntax in tests and verify installed version does not impact lock generation Particularly the test tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test is interesting because it verifies that an older version will be installed on update if the new one is only present in the installed repo or vendor dir. This was the cause of a lot of weird edge cases and unreliable update behavior in Composer v1 --- .../install-missing-alias-from-lock.test | 2 +- ...e-downgrades-non-whitelisted-unstable.test | 5 ++-- .../installer/update-whitelist-patterns.test | 25 +++++++++++++++++-- .../update-whitelist-reads-lock.test | 4 +-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test index 5acb7a069..5f6459799 100644 --- a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test @@ -29,7 +29,7 @@ Installing an old alias that doesn't exist anymore from a lock is possible "type": "library" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index 9b8d32f06..add46b848 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -53,7 +53,7 @@ update c/uptodate "packages": [ { "name": "a/old", "version": "1.0.0", "type": "library" }, { "name": "b/unstable", "version": "1.0.0", "type": "library" }, - { "name": "c/uptodate", "version": "2.0.0", "type": "library" }, + { "name": "c/uptodate", "version": "1.0.0", "type": "library" }, { "name": "d/removed", "version": "1.0.0", "type": "library" } ], "packages-dev": [], @@ -66,6 +66,7 @@ update c/uptodate "platform-dev": [] } --EXPECT-- -Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) Updating a/old (0.9.0) to a/old (1.0.0) +Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) +Updating c/uptodate (2.0.0) to c/uptodate (1.0.0) Installing d/removed (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test index de1fb1b73..d1b4fd66d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test @@ -39,10 +39,31 @@ Update with a package whitelist only updates those corresponding to the pattern { "name": "another/another", "version": "1.0" }, { "name": "no/regexp", "version": "1.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "vendor/Test-Package", "version": "1.0" }, + { "name": "vendor/NotMe", "version": "1.0" }, + { "name": "exact/Test-Package", "version": "1.0" }, + { "name": "notexact/TestPackage", "version": "1.0" }, + { "name": "all/Package1", "version": "1.0" }, + { "name": "all/Package2", "version": "1.0" }, + { "name": "another/another", "version": "1.0" }, + { "name": "no/regexp", "version": "1.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update vendor/Test* exact/Test-Package notexact/Test all/* no/reg?xp --EXPECT-- -Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0) -Updating exact/Test-Package (1.0) to exact/Test-Package (2.0) Updating all/Package1 (1.0) to all/Package1 (2.0) Updating all/Package2 (1.0) to all/Package2 (2.0) +Updating exact/Test-Package (1.0) to exact/Test-Package (2.0) +Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test index c84f0e65d..552051565 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test @@ -28,7 +28,7 @@ Limited update takes rules from lock if available, and not from the installed re { "name": "toupdate/installed", "version": "1.0.0" }, { "name": "toupdate/notinstalled", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -43,6 +43,6 @@ Limited update takes rules from lock if available, and not from the installed re --RUN-- update toupdate/installed --EXPECT-- -Updating toupdate/installed (1.0.0) to toupdate/installed (1.1.0) Updating old/installed (0.9.0) to old/installed (1.0.0) +Updating toupdate/installed (1.0.0) to toupdate/installed (1.1.0) Installing toupdate/notinstalled (1.0.0) From e6e317bc27c06c5a74f48e04ffb343796f5a1a8d Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 30 Oct 2019 00:40:36 +0100 Subject: [PATCH 118/321] Fix test configurations, missing lock files, invalid ones aliased alias test is failing because double alias is improperly resolved now --- .../installer/install-aliased-alias.test | 2 +- .../installer/install-from-empty-lock.test | 4 ++-- .../installer/partial-update-from-lock.test | 2 +- ...irements-do-not-affect-locked-versions.test | 2 +- ...e-whitelist-patterns-with-dependencies.test | 18 ++++++++++++++++++ .../update-whitelist-with-dependencies.test | 16 ++++++++++++++++ ...updating-dev-updates-url-and-reference.test | 2 +- 7 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test index f535caa7e..2c047f8bb 100644 --- a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test +++ b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test @@ -31,6 +31,6 @@ Installing double aliased package install --EXPECT-- Installing b/b (dev-foo) +Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) Marking b/b (dev-master) as installed, alias of b/b (dev-foo) Installing a/a (dev-master) -Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test index 7bb69f131..c3abd2377 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test @@ -21,7 +21,7 @@ Requirements from the composer file are not installed if the lock file is presen "packages": [ { "name": "required", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -31,4 +31,4 @@ Requirements from the composer file are not installed if the lock file is presen --RUN-- install --EXPECT-- -Installing required (1.0.0) \ No newline at end of file +Installing required (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test index ed2002e4e..f49f23dfd 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -74,8 +74,8 @@ update b/unstable "platform-dev": [] } --EXPECT-- -Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) Updating a/old (0.9.0) to a/old (1.0.0) +Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) Updating c/uptodate (2.0.0) to c/uptodate (1.0.0) Installing d/removed (1.0.0) Installing e/newreq (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test index 15d1b4ef5..8868cbe13 100644 --- a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test +++ b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test @@ -23,7 +23,7 @@ The locked version will not get overwritten by an install { "name": "foo/bar", "version": "1.0.0" }, { "name": "foo/baz", "version": "2.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test index c685f14ce..aee2310ac 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test @@ -41,6 +41,24 @@ Update with a package whitelist only updates those packages and their dependenci { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted-component1", "version": "1.0.0" }, + { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "root-dependency", "version": "1.0.0" }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} --RUN-- update whitelisted-* --with-dependencies --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test index bb2e04193..73acfa516 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test @@ -33,6 +33,22 @@ Update with a package whitelist only updates those packages and their dependenci { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} --RUN-- update whitelisted --with-dependencies --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test index c5c838517..06870c4ee 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -28,7 +28,7 @@ Updating a dev package for new reference updates the url and reference "dist": { "reference": "oldref", "url": "oldurl", "type": "zip", "shasum": "" } } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": {"a/a":20}, From eaae360ce61857821cfd027c6fc3e5642a4c9c98 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 30 Oct 2019 00:55:11 +0100 Subject: [PATCH 119/321] Correcting lock files in test cases and updating output --- src/Composer/Package/Locker.php | 2 +- .../Fixtures/installer/abandoned-listed.test | 8 ++++++-- .../Fixtures/installer/suggest-replaced.test | 8 ++++++-- .../Fixtures/installer/update-alias-lock.test | 2 +- .../update-picks-up-change-of-vcs-type.test | 10 +++++++++- ...whitelist-patterns-without-dependencies.test | 17 +++++++++++++++++ ...date-whitelist-with-dependency-conflict.test | 16 ++++++++++++++++ 7 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index d790837a5..577f9caa3 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -162,7 +162,7 @@ class Locker if (isset($lockData['packages-dev'])) { $lockedPackages = array_merge($lockedPackages, $lockData['packages-dev']); } else { - throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or run update to install those packages.'); + throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or delete it and run composer update to generate a new lock file.'); } } diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index 7eba0a6f0..d5e3c3d52 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -25,11 +25,15 @@ Abandoned packages are flagged install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Installing c/c (1.0.0) + - Installing a/a (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals Package a/a is abandoned, you should avoid using it. No replacement was suggested. Package c/c is abandoned, you should avoid using it. Use b/b instead. -Writing lock file Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index f18054d74..3ffcd20f7 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -20,9 +20,13 @@ Suggestions are not displayed for packages if they are replaced install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 2 installs, 0 updates, 0 removals +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Installing c/c (1.0.0) + - Installing a/a (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index f4f5e98eb..40a75807a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -36,7 +36,7 @@ Update aliased package does not mess up the lock file { "package": "a/a", "version": "dev-master", "source-reference": "1234" }, { "package": "a/a", "version": "dev-master", "alias-pretty-version": "1.0.x-dev", "alias-version": "1.0.9999999.9999999-dev" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test index 1e528d047..dfb3f650d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test +++ b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test @@ -31,7 +31,15 @@ Converting from one VCS type to another (including an URL change) should update "name": "a/a", "version": "1.0.0", "source": { "reference": "old-hg-ref", "type": "hg", "url": "old-hg-url" } } - ] + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] } --RUN-- update diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test index e5551b43f..ac07f23fd 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test @@ -37,6 +37,23 @@ Update with a package whitelist only updates those packages matching the pattern { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted-component1", "version": "1.0.0" }, + { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} --RUN-- update whitelisted-* --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test index f63229fbc..38a7bbf54 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test @@ -33,6 +33,22 @@ Update with a package whitelist only updates whitelisted packages if no dependen { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} --RUN-- update whitelisted --EXPECT-- From 6925005ac9c115d8105289a3bc9526bc765d2b21 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 17:35:44 +0100 Subject: [PATCH 120/321] Implement update mirrors/nothing/lock as its own installer mode These special commands no longer (ab)use the partial update mechanism but rather create a special install request for all current lock file contents and later override any modified code references to the originals. This leads to up to date remote metadata but no other changes. --- src/Composer/Command/UpdateCommand.php | 16 +++++- .../DependencyResolver/LockTransaction.php | 10 +++- .../DependencyResolver/PoolBuilder.php | 16 +----- src/Composer/Installer.php | 53 +++++++++++-------- src/Composer/Package/AliasPackage.php | 5 ++ src/Composer/Package/Package.php | 17 ++++++ src/Composer/Package/PackageInterface.php | 9 ++++ .../installer/update-mirrors-changes-url.test | 23 +++----- .../update-picks-up-change-of-vcs-type.test | 2 +- tests/Composer/Test/InstallerTest.php | 10 +++- 10 files changed, 104 insertions(+), 57 deletions(-) diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 99bd2d74b..0c3d3e6c7 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -121,6 +121,19 @@ EOT } } + // the arguments lock/nothing/mirrors are not package names but trigger a mirror update instead + // they are further mutually exclusive with listing actual package names + $filteredPackages = array_filter($packages, function ($package) { + return !in_array($package, array('lock', 'nothing', 'mirrors'), true); + }); + $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); + $packages = $filteredPackages; + + if ($updateMirrors && !empty($packages)) { + $io->writeError('You cannot simultaneously update only a selection of packages and regenerate the lock file metadata.'); + return -1; + } + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); @@ -146,7 +159,8 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages) + ->setUpdateMirrors($updateMirrors) + ->setUpdateWhitelist($packages) ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 75b2efb5b..7255de3dd 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -152,11 +152,19 @@ class LockTransaction } // TODO additionalFixedRepository needs to be looked at here as well? - public function getNewLockPackages($devMode) + public function getNewLockPackages($devMode, $updateMirrors = false) { $packages = array(); foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is + if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) { + foreach ($this->presentMap as $presentPackage) { + if ($package->getName() == $presentPackage->getName() && $package->getVersion() == $presentPackage->getVersion() && $presentPackage->getSourceReference()) { + $package->setSourceDistReferences($presentPackage->getSourceReference()); + } + } + } $packages[] = $package; } } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 39ac5098f..1c995bc4e 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -184,7 +184,7 @@ class PoolBuilder if (isset($this->rootReferences[$name])) { // do not modify the references on already locked packages if (!$request->isFixedPackage($package)) { - $this->setReferences($package, $this->rootReferences[$name]); + $package->setSourceDistReferences($this->rootReferences[$name]); } } @@ -225,19 +225,5 @@ class PoolBuilder return $loadNames; } - - private function setReferences(Package $package, $reference) - { - $package->setSourceReference($reference); - - // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL - // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this? - if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { - $package->setDistReference($reference); - $package->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $package->getDistUrl())); - } elseif ($package->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it - $package->setDistReference($reference); - } - } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index dea1e03e5..86d251756 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -38,6 +38,7 @@ use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\CompletePackage; use Composer\Package\Link; +use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Package; @@ -137,6 +138,7 @@ class Installer * * @var array|null */ + protected $updateMirrors = false; protected $updateWhitelist = null; protected $whitelistTransitiveDependencies = false; protected $whitelistAllDependencies = false; @@ -192,6 +194,10 @@ class Installer gc_collect_cycles(); gc_disable(); + if ($this->updateWhitelist && $this->updateMirrors) { + throw new \RuntimeException("The installer options updateMirrors and updateWhitelist are mutually exclusive."); + } + // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { // TODO throw an error instead? @@ -370,8 +376,15 @@ class Installer $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); - foreach ($links as $link) { - $request->install($link->getTarget(), $link->getConstraint()); + // if we're updating mirrors we want to keep exactly the same versions installed which are in the lock file, but we want current remote metadata + if ($this->updateMirrors) { + foreach ($lockedRepository->getPackages() as $lockedPackage) { + $request->install($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); + } + } else { + foreach ($links as $link) { + $request->install($link->getTarget(), $link->getConstraint()); + } } // if the updateWhitelist is enabled, packages not in it are also fixed @@ -489,8 +502,8 @@ class Installer } $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockPackages(false), - $lockTransaction->getNewLockPackages(true), + $lockTransaction->getNewLockPackages(false, $this->updateMirrors), + $lockTransaction->getNewLockPackages(true, $this->updateMirrors), $platformReqs, $platformDevReqs, $aliases, @@ -912,23 +925,6 @@ class Installer return $normalizedAliases; } - // TODO do we still need this function? - private function updateInstallReferences(PackageInterface $package, $reference) - { - if (!$reference) { - return; - } - - $package->setSourceReference($reference); - - if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { - $package->setDistReference($reference); - $package->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $package->getDistUrl())); - } elseif ($package->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it - $package->setDistReference($reference); - } - } - /** * @param PlatformRepository $platformRepo * @param array $aliases @@ -1044,7 +1040,7 @@ class Installer $depPackages = array_merge($depPackages, call_user_func_array('array_merge', $matchesByPattern)); } - if (count($depPackages) == 0 && !$nameMatchesRequiredPackage && !in_array($packageName, array('nothing', 'lock', 'mirrors'))) { + if (count($depPackages) == 0 && !$nameMatchesRequiredPackage) { $this->io->writeError('Package "' . $packageName . '" listed for update is not installed. Ignoring.'); } @@ -1347,6 +1343,19 @@ class Installer return $this; } + /** + * Update the lock file to the exact same versions and references but use current remote metadata like URLs and mirror info + * + * @param bool $updateMirrors + * @return Installer + */ + public function setUpdateMirrors($updateMirrors) + { + $this->updateMirrors = $updateMirrors; + + return $this; + } + /** * restrict the update operation to a few packages, all other packages * that are already installed will be kept at their current version diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index 89f197856..b103139dd 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -411,4 +411,9 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->setDistType($type); } + + public function setSourceDistReferences($reference) + { + return $this->aliasOf->setSourceDistReferences($reference); + } } diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index 6c7b426e7..c633e1856 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -569,6 +569,23 @@ class Package extends BasePackage return $this->archiveExcludes; } + /** + * {@inheritDoc} + */ + public function setSourceDistReferences($reference) + { + $this->setSourceReference($reference); + + // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL + // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this? + if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $this->getDistUrl())) { + $this->setDistReference($reference); + $this->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $this->getDistUrl())); + } elseif ($this->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it + $this->setDistReference($reference); + } + } + /** * Replaces current version and pretty version with passed values. * It also sets stability. diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 25a2e9bfe..7e83839ff 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -386,4 +386,13 @@ interface PackageInterface * @return void */ public function setDistReference($reference); + + /** + * Set dist and source references and update dist URL for ones that contain a reference + * + * @param string $reference + * + * @return void + */ + public function setSourceDistReferences($reference); } diff --git a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test index 9d88870b0..9bfca4c85 100644 --- a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test @@ -149,14 +149,14 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "packages": [ { "name": "a/a", "version": "dev-master", - "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, - "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/newa", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/newa/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, { "name": "b/b", "version": "2.0.3", - "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" }, - "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/newb", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/newb/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, { @@ -171,12 +171,6 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, - { - "name": "e/e", "version": "dev-master", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/e/newe", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/zipball/1111111111111111111111111111111111111111", "type": "zip" }, - "type": "library" - }, { "name": "f/f", "version": "dev-master", "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" }, @@ -185,8 +179,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, { "name": "g/g", "version": "dev-master", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/g/newg", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/newg", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/newg/zipball/0000000000000000000000000000000000000000", "type": "zip" }, "type": "library" } ], @@ -206,8 +200,5 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "platform-dev": [] } --RUN-- -update a/a b/b d/d g/g +update mirrors --EXPECT-- -Installing e/e (dev-master 1111111) -Updating a/a (dev-master 1111111) to a/a (dev-master 2222222) -Updating g/g (dev-master 0000000) to g/g (dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test index dfb3f650d..a82487a31 100644 --- a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test +++ b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test @@ -42,7 +42,7 @@ Converting from one VCS type to another (including an URL change) should update "platform-dev": [] } --RUN-- -update +update mirrors --EXPECT-LOCK-- { "packages": [ diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index ce87d111f..f1e55a794 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -269,11 +269,19 @@ class InstallerTest extends TestCase }); $application->get('update')->setCode(function ($input, $output) use ($installer) { + $packages = $input->getArgument('packages'); + $filteredPackages = array_filter($packages, function ($package) { + return !in_array($package, array('lock', 'nothing', 'mirrors'), true); + }); + $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); + $packages = $filteredPackages; + $installer ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) ->setDryRun($input->getOption('dry-run')) - ->setUpdateWhitelist($input->getArgument('packages')) + ->setUpdateMirrors($updateMirrors) + ->setUpdateWhitelist($packages) ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) ->setPreferStable($input->getOption('prefer-stable')) From 2d37bb4116f5092302266419746a003c32daa866 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 17:38:16 +0100 Subject: [PATCH 121/321] Outated lock files now trigger an error requesting removal rather than being ignored silently --- .../Test/Fixtures/installer/update-alias-lock.test | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index 40a75807a..da4faeed8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -29,20 +29,6 @@ Update aliased package does not mess up the lock file }, "minimum-stability": "dev" } ---LOCK-- -{ - "_": "outdated lock file, should not have to be loaded in an update", - "packages": [ - { "package": "a/a", "version": "dev-master", "source-reference": "1234" }, - { "package": "a/a", "version": "dev-master", "alias-pretty-version": "1.0.x-dev", "alias-version": "1.0.9999999.9999999-dev" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false -} --INSTALLED-- [ { From cc274ebdf4417f13a7197efcfb72aa34bea32733 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 17:42:17 +0100 Subject: [PATCH 122/321] Do not reset references on update mirrors if VCS type has changed --- src/Composer/DependencyResolver/LockTransaction.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 7255de3dd..293f4a43b 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -158,9 +158,14 @@ class LockTransaction foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is + // we do not reset references if the currently present package didn't have any, or if the type of VCS has changed if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) { foreach ($this->presentMap as $presentPackage) { - if ($package->getName() == $presentPackage->getName() && $package->getVersion() == $presentPackage->getVersion() && $presentPackage->getSourceReference()) { + if ($package->getName() == $presentPackage->getName() && + $package->getVersion() == $presentPackage->getVersion() && + $presentPackage->getSourceReference() && + $presentPackage->getSourceType() === $package->getSourceType() + ) { $package->setSourceDistReferences($presentPackage->getSourceReference()); } } From c50d236378c2538c6d08b66ea733c99e29a52553 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 20:40:28 +0100 Subject: [PATCH 123/321] Correctly load branch aliases from lock file Root aliases are also stored in the lock file, so on install do not read them from composer.json. --- src/Composer/DependencyResolver/RuleSetGenerator.php | 2 +- src/Composer/Installer.php | 5 ++++- src/Composer/Package/Locker.php | 5 +++++ .../Test/Fixtures/installer/install-aliased-alias.test | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index d42617a7a..76f9270be 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -262,7 +262,7 @@ class RuleSetGenerator foreach ($request->getFixedPackages() as $package) { if ($package->id == -1) { - throw new \RuntimeException("Fixed package ".$package->getName()." was not added to solver pool."); + throw new \RuntimeException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool."); } $this->addRulesForPackage($package, $ignorePlatformReqs); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 86d251756..8cdf803c5 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -658,7 +658,8 @@ class Installer // creating repository set $policy = $this->createPolicy(false); - $repositorySet = $this->createRepositorySet($platformRepo, $aliases, $lockedRepository); + // use aliases from lock file only, so empty root aliases here + $repositorySet = $this->createRepositorySet($platformRepo, array(), $lockedRepository); $repositorySet->addRepository($lockedRepository); $this->io->writeError('Installing dependencies from lock file'.($this->devMode ? ' (including require-dev)' : '').''); @@ -708,6 +709,8 @@ class Installer // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); + } else { + // need to still create the pool to reconstruct aliases } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 577f9caa3..8069ba50c 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -176,6 +176,11 @@ class Locker $package = $this->loader->load($info); $packages->addPackage($package); $packageByName[$package->getName()] = $package; + + if ($package instanceof AliasPackage) { + $packages->addPackage($package->getAliasOf()); + $packageByName[$package->getAliasOf()->getName()] = $package->getAliasOf(); + } } if (isset($lockData['aliases'])) { diff --git a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test index 2c047f8bb..63410283d 100644 --- a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test +++ b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test @@ -31,6 +31,6 @@ Installing double aliased package install --EXPECT-- Installing b/b (dev-foo) -Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) Marking b/b (dev-master) as installed, alias of b/b (dev-foo) +Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) Installing a/a (dev-master) From e308f043b983ef7893d478fd99ccdeeade302eff Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 21:18:09 +0100 Subject: [PATCH 124/321] Fully switch to spl_object_hash in lock transaction The pool builder tries to be minimal so it's fine for present/locked packages not be assigned a solver/pool id. Adding a test to verify correct creation of uninstall jobs --- .../DependencyResolver/LockTransaction.php | 34 ++++++---- .../update-removes-unused-locked-dep.test | 67 +++++++++++++++++++ 2 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 293f4a43b..9115e1533 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -67,6 +67,7 @@ class LockTransaction protected function calculateOperations() { $operations = array(); + $ignoreRemove = array(); $lockMeansUpdateMap = $this->findPotentialUpdates(); foreach ($this->decisions as $i => $decision) { @@ -77,17 +78,17 @@ class LockTransaction // wanted & !present if ($literal > 0 && !isset($this->presentMap[spl_object_hash($package)])) { - if (isset($lockMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { + if (isset($lockMeansUpdateMap[spl_object_hash($package)]) && !$package instanceof AliasPackage) { // TODO we end up here sometimes because we prefer the remote package now to get up to date metadata // TODO define some level of identity here for what constitutes an update and what can be ignored? new kind of metadata only update? - $target = $lockMeansUpdateMap[abs($literal)]; + $target = $lockMeansUpdateMap[spl_object_hash($package)]; if ($package->getName() !== $target->getName() || $package->getVersion() !== $target->getVersion()) { $operations[] = new Operation\UpdateOperation($target, $package, $reason); } // avoid updates to one package from multiple origins - $ignoreRemove[$lockMeansUpdateMap[abs($literal)]->id] = true; - unset($lockMeansUpdateMap[abs($literal)]); + $ignoreRemove[spl_object_hash($lockMeansUpdateMap[spl_object_hash($package)])] = true; + unset($lockMeansUpdateMap[spl_object_hash($package)]); } else { if ($package instanceof AliasPackage) { $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason); @@ -103,7 +104,7 @@ class LockTransaction $reason = $decision[Decisions::DECISION_REASON]; $package = $this->pool->literalToPackage($literal); - if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[$package->id])) { + if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[spl_object_hash($package)])) { if ($package instanceof AliasPackage) { $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason); } else { @@ -112,6 +113,17 @@ class LockTransaction } } + foreach ($this->presentMap as $package) { + if ($package->id === -1 && !isset($ignoreRemove[spl_object_hash($package)])) { + // TODO pass reason parameter to these two operations? + if ($package instanceof AliasPackage) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package); + } else { + $operations[] = new Operation\UninstallOperation($package); + } + } + } + $this->setResultPackages(); return $operations; @@ -207,15 +219,11 @@ class LockTransaction // TODO can't we just look at existing rules? $updates = $this->policy->findUpdatePackages($this->pool, $package); - $literals = array($package->id); + $updatesAndPackage = array_merge(array($package), $updates); - foreach ($updates as $update) { - $literals[] = $update->id; - } - - foreach ($literals as $updateLiteral) { - if (!isset($lockMeansUpdateMap[$updateLiteral])) { - $lockMeansUpdateMap[$updateLiteral] = $package; + foreach ($updatesAndPackage as $update) { + if (!isset($lockMeansUpdateMap[spl_object_hash($update)])) { + $lockMeansUpdateMap[spl_object_hash($update)] = $package; } } } diff --git a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test new file mode 100644 index 000000000..808afb02e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test @@ -0,0 +1,67 @@ +--TEST-- +A composer update should remove unused locked dependencies from the lock file and remove unused installed deps from disk +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ] + } + ], + "require": { + "a/a": "*" + } +} +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" }, + { "name": "c/c", "version": "1.0.0" } +] +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0", "type": "library" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Lock file operations: 0 installs, 0 updates, 1 removal + - Uninstalling b/b (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 0 updates, 2 removals +Generating autoload files + +--EXPECT-- +Uninstalling c/c (1.0.0) +Uninstalling b/b (1.0.0) From e6e07231057d910e5d3d5ed105a54ce7f9742e64 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 21:25:43 +0100 Subject: [PATCH 125/321] Remove unnecessary comments, aliases in lock file are correctly created --- src/Composer/Installer.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8cdf803c5..493989bab 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -709,12 +709,9 @@ class Installer // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); - } else { - // need to still create the pool to reconstruct aliases } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? - $localRepoTransaction = new LocalRepoTransaction($lockedRepository, $localRepo); if (!$localRepoTransaction->getOperations()) { From 26da52227ebac871a1a1eeddc0c10cbb9a35d38c Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 21:51:53 +0100 Subject: [PATCH 126/321] Clean up the Solver tests, no more installed repo input and new sorting The solver now only calculates a lock file transaction which does not need to be sorted in order of dependencies. This is only necessary for the local repo transaction generated without the solver during install --- src/Composer/Installer.php | 2 + src/Composer/Plugin/PluginManager.php | 2 +- .../DependencyResolver/DefaultPolicyTest.php | 6 +- .../Test/DependencyResolver/SolverTest.php | 92 ++++++++----------- 4 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 493989bab..982f9baae 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -367,6 +367,8 @@ class Installer if (!$repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { $constraint = new Constraint('=', $lockedPackage->getVersion()); $constraint->setPrettyString('(stability not acceptable)'); + + // if we can get rid of this remove() here, we can generally get rid of remove support in the request $request->remove($lockedPackage->getName(), $constraint); } } diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 814b0218a..03b872d47 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -158,7 +158,7 @@ class PluginManager $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($localRepo); if ($globalRepo) { $repositorySet->addRepository($globalRepo); diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 919d098e3..b4df12310 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -35,7 +35,7 @@ class DefaultPolicyTest extends TestCase public function setUp() { - $this->repositorySet = new RepositorySet(array(), 'dev'); + $this->repositorySet = new RepositorySet(array(), array(), 'dev'); $this->repo = new ArrayRepository; $this->repoLocked = new ArrayRepository; @@ -181,7 +181,7 @@ class DefaultPolicyTest extends TestCase $this->assertSame($expected, $selected); - $this->repositorySet = new RepositorySet(array(), 'dev'); + $this->repositorySet = new RepositorySet(array(), array(), 'dev'); $this->repositorySet->addRepository($repo2); $this->repositorySet->addRepository($repo1); @@ -287,7 +287,7 @@ class DefaultPolicyTest extends TestCase $repo->addPackage($packageA = clone $packageA); $repo->addPackage($packageB = clone $packageB); - $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($this->repo); $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer')); diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 3491f9c05..8f12f3122 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -29,7 +29,6 @@ class SolverTest extends TestCase { protected $repoSet; protected $repo; - protected $repoInstalled; protected $repoLocked; protected $request; protected $policy; @@ -39,10 +38,9 @@ class SolverTest extends TestCase { $this->repoSet = new RepositorySet(array()); $this->repo = new ArrayRepository; - $this->repoInstalled = new InstalledArrayRepository; $this->repoLocked = new ArrayRepository; - $this->request = new Request(); + $this->request = new Request($this->repoLocked); $this->policy = new DefaultPolicy; } @@ -58,10 +56,9 @@ class SolverTest extends TestCase )); } - public function testSolverRemoveIfNotInstalled() + public function testSolverRemoveIfNotRequested() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoLocked->addPackage(clone $packageA); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); $this->checkSolverResult(array( @@ -96,7 +93,6 @@ class SolverTest extends TestCase $repo1->addPackage($foo1 = $this->getPackage('foo', '1')); $repo2->addPackage($foo2 = $this->getPackage('foo', '1')); - $this->repoSet->addRepository($this->repoInstalled); $this->repoSet->addRepository($repo1); $this->repoSet->addRepository($repo2); @@ -172,36 +168,36 @@ class SolverTest extends TestCase $this->request->install('C'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA), )); } - public function testSolverInstallInstalled() + public function testSolverFixLocked() { - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); + $this->request->fixPackage($packageA); $this->checkSolverResult(array()); } - public function testSolverInstallInstalledWithAlternative() + public function testSolverFixLockedWithAlternative() { $this->repo->addPackage($this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); + $this->request->fixPackage($packageA); $this->checkSolverResult(array()); } public function testSolverRemoveSingle() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); $this->request->remove('A'); @@ -223,17 +219,15 @@ class SolverTest extends TestCase public function testSolverUpdateDoesOnlyUpdate() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); $this->reposComplete(); $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires'))); - $this->request->install('A', $this->getVersionConstraint('=', '1.0.0.0')); + $this->request->fixPackage($packageA); $this->request->install('B', $this->getVersionConstraint('=', '1.1.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); - $this->request->update('B', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -242,12 +236,11 @@ class SolverTest extends TestCase public function testSolverUpdateSingle() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); $this->reposComplete(); $this->request->install('A'); - $this->request->update('A'); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageA, 'to' => $newPackageA), @@ -256,8 +249,8 @@ class SolverTest extends TestCase public function testSolverUpdateAll() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); @@ -267,7 +260,6 @@ class SolverTest extends TestCase $this->reposComplete(); $this->request->install('A'); - $this->request->updateAll(); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -277,28 +269,26 @@ class SolverTest extends TestCase public function testSolverUpdateCurrent() { - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($this->getPackage('A', '1.0')); $this->repo->addPackage($this->getPackage('A', '1.0')); $this->reposComplete(); $this->request->install('A'); - $this->request->update('A'); $this->checkSolverResult(array()); } public function testSolverUpdateOnlyUpdatesSelectedPackage() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($packageAnewer = $this->getPackage('A', '1.1')); $this->repo->addPackage($packageBnewer = $this->getPackage('B', '1.1')); $this->reposComplete(); $this->request->install('A'); - $this->request->install('B'); - $this->request->update('A'); + $this->request->fixPackage($packageB); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageA, 'to' => $packageAnewer), @@ -307,13 +297,12 @@ class SolverTest extends TestCase public function testSolverUpdateConstrained() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A'); $this->checkSolverResult(array(array( 'job' => 'update', @@ -324,13 +313,12 @@ class SolverTest extends TestCase public function testSolverUpdateFullyConstrained() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -341,14 +329,13 @@ class SolverTest extends TestCase public function testSolverUpdateFullyConstrainedPrunesInstalledPackages() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array( array( @@ -365,8 +352,8 @@ class SolverTest extends TestCase public function testSolverAllJobs() { - $this->repoInstalled->addPackage($packageD = $this->getPackage('D', '1.0')); - $this->repoInstalled->addPackage($oldPackageC = $this->getPackage('C', '1.0')); + $this->repoLocked->addPackage($packageD = $this->getPackage('D', '1.0')); + $this->repoLocked->addPackage($oldPackageC = $this->getPackage('C', '1.0')); $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); @@ -379,12 +366,11 @@ class SolverTest extends TestCase $this->request->install('A'); $this->request->install('C'); - $this->request->update('C'); $this->request->remove('D'); $this->checkSolverResult(array( - array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), array('job' => 'install', 'package' => $packageB), + array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), array('job' => 'install', 'package' => $packageA), array('job' => 'remove', 'package' => $packageD), )); @@ -411,7 +397,7 @@ class SolverTest extends TestCase public function testSolverObsolete() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); $packageB->setReplaces(array('a' => new Link('B', 'A', new MultiConstraint(array())))); @@ -540,8 +526,8 @@ class SolverTest extends TestCase $this->request->install('X'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $newPackageB), array('job' => 'install', 'package' => $packageA), + array('job' => 'install', 'package' => $newPackageB), array('job' => 'install', 'package' => $packageX), )); } @@ -584,9 +570,9 @@ class SolverTest extends TestCase $this->request->install('C'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), - array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageC), + array('job' => 'install', 'package' => $packageA), )); } @@ -766,7 +752,7 @@ class SolverTest extends TestCase $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n"; $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n"; $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; - $msg .= " - Can only install one of: B[0.9, 1.0].\n"; + $msg .= " - Same name, can only install one of: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; $this->assertEquals($msg, $e->getMessage()); @@ -820,8 +806,8 @@ class SolverTest extends TestCase $this->request->install('A', $this->getVersionConstraint('==', '1.1.0.0')); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA2), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA2), array('job' => 'install', 'package' => $packageA2Alias), )); } @@ -843,9 +829,9 @@ class SolverTest extends TestCase $this->request->install('B'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageAAlias), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA), )); } @@ -908,12 +894,12 @@ class SolverTest extends TestCase $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageF1), - array('job' => 'install', 'package' => $packageD), - array('job' => 'install', 'package' => $packageG2), array('job' => 'install', 'package' => $packageC2), + array('job' => 'install', 'package' => $packageG2), + array('job' => 'install', 'package' => $packageF1), array('job' => 'install', 'package' => $packageE), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageD), array('job' => 'install', 'package' => $packageA), )); @@ -939,7 +925,7 @@ class SolverTest extends TestCase $transaction = $this->solver->solve($this->request); $result = array(); - foreach ($transaction as $operation) { + foreach ($transaction->getOperations() as $operation) { if ('update' === $operation->getJobType()) { $result[] = array( 'job' => 'update', From bf99f1a341083533065a6214114c8ead262869f4 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 21:56:17 +0100 Subject: [PATCH 127/321] Fix RepositorySet constructor calls to use new signature --- src/Composer/Command/CreateProjectCommand.php | 2 +- src/Composer/Command/InitCommand.php | 2 +- src/Composer/Command/ShowCommand.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index a718bfa0b..07b39fd9b 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -293,7 +293,7 @@ EOT throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } - $repositorySet = new RepositorySet(array(), $stability); + $repositorySet = new RepositorySet(array(), array(), $stability); $repositorySet->addRepository($sourceRepo); $phpVersion = null; diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 08f891014..19ff99e9d 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -666,7 +666,7 @@ EOT $key = $minimumStability ?: 'default'; if (!isset($this->repositorySets[$key])) { - $this->repositorySets[$key] = $repositorySet = new RepositorySet(array(), $minimumStability ?: $this->getMinimumStability($input)); + $this->repositorySets[$key] = $repositorySet = new RepositorySet(array(), array(), $minimumStability ?: $this->getMinimumStability($input)); $repositorySet->addRepository($this->getRepos()); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 91ee27143..dde0f0062 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -538,7 +538,7 @@ EOT $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version; $policy = new DefaultPolicy(); - $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($repos); $matchedPackage = null; @@ -1002,7 +1002,7 @@ EOT private function getRepositorySet(Composer $composer) { if (!$this->repositorySet) { - $this->repositorySet = new RepositorySet(array(), $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); + $this->repositorySet = new RepositorySet(array(), array(), $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); } From 737a613a50c7dd8eebcf7a1bc8ed7e0d7b46afdd Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 22:01:37 +0100 Subject: [PATCH 128/321] Use array() instead of [] for PHP 5.3 compat --- tests/Composer/Test/InstallerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index f1e55a794..95fe7b89e 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -125,7 +125,7 @@ class InstallerTest extends TestCase { $dumper = new ArrayDumper(); - $comparable = []; + $comparable = array(); foreach ($packages as $package) { $comparable[] = $dumper->dump($package); } From 28596d9c12c43db91f61d69a708dd937531e4722 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 22:02:35 +0100 Subject: [PATCH 129/321] Define property which is later accessed in local repo transaction --- src/Composer/DependencyResolver/LocalRepoTransaction.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index a41798b50..50056e953 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -33,6 +33,9 @@ class LocalRepoTransaction /** @var RepositoryInterface */ protected $localRepository; + /** @var array */ + protected $operations; + /** * Reassigns ids for all packages in the lockedrepository */ From bd6b4e433c6118bf286b8ee7bc9cae9bd715b47e Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 22:09:24 +0100 Subject: [PATCH 130/321] Use JsonFile::JSON_PRETTY_PRINT instead of php const for PHP 5.3 compat --- tests/Composer/Test/InstallerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 95fe7b89e..758121979 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -93,7 +93,7 @@ class InstallerTest extends TestCase $lockJsonMock->expects($this->any()) ->method('write') ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { - $lockData = json_encode($value, JSON_PRETTY_PRINT); + $lockData = json_encode($value, JsonFile::JSON_PRETTY_PRINT); })); $tempLockData = null; @@ -216,7 +216,7 @@ class InstallerTest extends TestCase $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); // emulate a writable lock file - $lockData = $lock ? json_encode($lock, JSON_PRETTY_PRINT): null; + $lockData = $lock ? json_encode($lock, JsonFile::JSON_PRETTY_PRINT): null; $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); $lockJsonMock->expects($this->any()) ->method('read') @@ -231,7 +231,7 @@ class InstallerTest extends TestCase $lockJsonMock->expects($this->any()) ->method('write') ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { - $lockData = json_encode($value, JSON_PRETTY_PRINT); + $lockData = json_encode($value, JsonFile::JSON_PRETTY_PRINT); })); if ($expectLock) { From 0099f5636161308ef300d9c87dbc153bb936ea37 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 22:11:54 +0100 Subject: [PATCH 131/321] Define property which is later accessed in lock transaction --- src/Composer/DependencyResolver/LockTransaction.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 9115e1533..76468ddc6 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -40,11 +40,14 @@ class LockTransaction * @var array */ protected $unlockableMap; - protected $decisions; - protected $resultPackages; + /** + * @var array + */ + protected $operations; + public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions) { $this->policy = $policy; From 3b26ef0f1bd9629fdd61eca3d2a010b74803e76d Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 12:13:23 +0100 Subject: [PATCH 132/321] clean up extract dev packages --- src/Composer/Installer.php | 77 ++------------------------------------ 1 file changed, 3 insertions(+), 74 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 982f9baae..c5e12b02e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -557,14 +557,14 @@ class Installer $pool = $repositorySet->createPool($request); - // solve dependencies + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); $solver = new Solver($policy, $pool, $this->io); try { $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); $solver = null; } catch (SolverProblemsException $e) { - // TODO change info message here - $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); $this->io->writeError($e->getMessage()); return max(1, $e->getCode()); @@ -573,77 +573,6 @@ class Installer $lockTransaction->setNonDevPackages($nonDevLockTransaction); } - - // TODO add proper output and events to above function based on old version below - /** - * Extracts the dev packages out of the localRepo - * - * This works by faking the operations so we can see what the dev packages - * would be at the end of the operation execution. This lets us then remove - * the dev packages from the list of operations accordingly if we are in a - * --no-dev install or update. - * - * @return array - private function extractDevPackages(array $operations, RepositoryInterface $localRepo, PlatformRepository $platformRepo, array $aliases) - { - // fake-apply all operations to this clone of the local repo so we see the complete set of package we would end up with - $tempLocalRepo = clone $localRepo; - foreach ($operations as $operation) { - switch ($operation->getJobType()) { - case 'install': - case 'markAliasInstalled': - if (!$tempLocalRepo->hasPackage($operation->getPackage())) { - $tempLocalRepo->addPackage(clone $operation->getPackage()); - } - break; - case 'uninstall': - case 'markAliasUninstalled': - $tempLocalRepo->removePackage($operation->getPackage()); - break; - case 'update': - $tempLocalRepo->removePackage($operation->getInitialPackage()); - if (!$tempLocalRepo->hasPackage($operation->getTargetPackage())) { - $tempLocalRepo->addPackage(clone $operation->getTargetPackage()); - } - break; - default: - throw new \LogicException('Unknown type: '.$operation->getJobType()); - } - } - // we have to reload the local repo to handle aliases properly - // but as it is not persisted on disk we use a loader/dumper - // to reload it in memory - $localRepo = new InstalledArrayRepository(array()); - $loader = new ArrayLoader(null, true); - $dumper = new ArrayDumper(); - foreach ($tempLocalRepo->getCanonicalPackages() as $pkg) { - $localRepo->addPackage($loader->load($dumper->dump($pkg))); - } - unset($tempLocalRepo, $loader, $dumper); - $policy = $this->createPolicy(); - $pool = $this->createPool(); - $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); - $pool->addRepository($installedRepo, $aliases); - // creating requirements request without dev requirements - $request = $this->createRequest($this->package, $platformRepo); - $request->updateAll(); - foreach ($this->package->getRequires() as $link) { - $request->install($link->getTarget(), $link->getConstraint()); - } - // solve deps to see which get removed - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); - $solver = new Solver($policy, $pool, $installedRepo, $this->io); - $ops = $solver->solve($request, $this->ignorePlatformReqs); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); - $devPackages = array(); - foreach ($ops as $op) { - if ($op->getJobType() === 'uninstall') { - $devPackages[] = $op->getPackage(); - } - } - return $devPackages; - }*/ - /** * @param RepositoryInterface $localRepo * @param RepositoryInterface $installedRepo From e26405d8583dfc7e6083d3af2c5255cfc5cc2e81 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 12:26:46 +0100 Subject: [PATCH 133/321] Clean up comments and output --- src/Composer/Installer.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index c5e12b02e..9fa0b1573 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -200,7 +200,7 @@ class Installer // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { - // TODO throw an error instead? + $this->io->writeError('No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.'); $this->update = true; } @@ -574,11 +574,9 @@ class Installer } /** - * @param RepositoryInterface $localRepo - * @param RepositoryInterface $installedRepo - * @param PlatformRepository $platformRepo - * @param array $aliases - * @return int exit code + * @param RepositoryInterface $localRepo + * @param bool $alreadySolved Whether the function is called as part of an update command or independently + * @return int exit code */ protected function doInstall(RepositoryInterface $localRepo, $alreadySolved = false) { @@ -625,14 +623,14 @@ class Installer $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); $solver = null; - // installing the locked packages on this platfom resulted in lock modifying operations, there wasn't a conflict, but the lock file as-is seems to not work on this system + // installing the locked packages on this platform resulted in lock modifying operations, there wasn't a conflict, but the lock file as-is seems to not work on this system if (0 !== count($lockTransaction->getOperations())) { - $this->io->writeError('Your lock file cannot be installed on this system without changes, please run composer update.', true, IOInterface::QUIET); + $this->io->writeError('Your lock file cannot be installed on this system without changes. Please run composer update.', true, IOInterface::QUIET); // TODO actually display operations to explain what happened? return 1; } } catch (SolverProblemsException $e) { - $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError('Your lock file does not contain a compatible set of packages. Please run composer update.', true, IOInterface::QUIET); $this->io->writeError($e->getMessage()); return max(1, $e->getCode()); From ff5ec54f0408ceb77c3d9398edd04d40dc9266f1 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 12:31:26 +0100 Subject: [PATCH 134/321] Correctly use install and update commands in our installer tests --- tests/Composer/Test/Fixtures/installer/abandoned-listed.test | 2 +- .../Test/Fixtures/installer/broken-deps-do-not-replace.test | 2 +- tests/Composer/Test/Fixtures/installer/github-issues-4319.test | 2 +- tests/Composer/Test/Fixtures/installer/suggest-installed.test | 2 +- tests/Composer/Test/Fixtures/installer/suggest-prod.test | 1 + tests/Composer/Test/Fixtures/installer/suggest-replaced.test | 2 +- tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test | 1 + 7 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index d5e3c3d52..cdf648c0d 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -22,7 +22,7 @@ Abandoned packages are flagged } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index a4bfe6a4d..3d5cba664 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -19,7 +19,7 @@ Broken dependencies should not lead to a replacer being installed which is not m } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index b387942fb..2fbd8784c 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -28,7 +28,7 @@ Present a clear error message when config.platform.php version results in a conf } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index 468a53612..6995f78e7 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -17,7 +17,7 @@ Suggestions are not displayed for installed packages } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index e28ef03e5..89d6ab8de 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -17,6 +17,7 @@ Suggestions are not displayed in non-dev mode --RUN-- install --no-dev --EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index 3ffcd20f7..62c13e560 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -17,7 +17,7 @@ Suggestions are not displayed for packages if they are replaced } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index ab22eeb6e..f45b710f0 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -17,6 +17,7 @@ Suggestions are displayed --RUN-- install --EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals From 25de5218c39957dc644eb9f03c265a0bb7060534 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 15:56:46 +0100 Subject: [PATCH 135/321] Reunify lock and local repo transaction code and apply the same sorting --- .../LocalRepoTransaction.php | 275 +--------------- .../DependencyResolver/LockTransaction.php | 141 +------- src/Composer/DependencyResolver/Solver.php | 2 +- .../DependencyResolver/Transaction.php | 310 ++++++++++++++++++ src/Composer/Installer.php | 17 - .../Test/DependencyResolver/SolverTest.php | 31 +- .../Fixtures/installer/abandoned-listed.test | 2 +- .../installer/github-issues-4795-2.test | 2 +- .../Fixtures/installer/suggest-installed.test | 2 +- .../update-with-all-dependencies.test | 2 +- 10 files changed, 346 insertions(+), 438 deletions(-) create mode 100644 src/Composer/DependencyResolver/Transaction.php diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index 50056e953..b22f555e5 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -24,278 +24,13 @@ use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann */ -class LocalRepoTransaction +class LocalRepoTransaction extends Transaction { - /** @var array */ - protected $lockedPackages; - protected $lockedPackagesByName = array(); - - /** @var RepositoryInterface */ - protected $localRepository; - - /** @var array */ - protected $operations; - - /** - * Reassigns ids for all packages in the lockedrepository - */ public function __construct(RepositoryInterface $lockedRepository, $localRepository) { - $this->localRepository = $localRepository; - $this->setLockedPackageMaps($lockedRepository); - $this->operations = $this->calculateOperations(); - } - - private function setLockedPackageMaps($lockedRepository) - { - $packageSort = function (PackageInterface $a, PackageInterface $b) { - // sort alias packages by the same name behind their non alias version - if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) { - return $a instanceof AliasPackage ? -1 : 1; - } - return strcmp($b->getName(), $a->getName()); - }; - - $id = 1; - $this->lockedPackages = array(); - foreach ($lockedRepository->getPackages() as $package) { - $package->id = $id++; - $this->lockedPackages[$package->id] = $package; - foreach ($package->getNames() as $name) { - $this->lockedPackagesByName[$name][] = $package; - } - } - - uasort($this->lockedPackages, $packageSort); - foreach ($this->lockedPackagesByName as $name => $packages) { - uasort($this->lockedPackagesByName[$name], $packageSort); - } - } - - public function getOperations() - { - return $this->operations; - } - - protected function calculateOperations() - { - $operations = array(); - - $localPackageMap = array(); - $removeMap = array(); - $localAliasMap = array(); - $removeAliasMap = array(); - foreach ($this->localRepository->getPackages() as $package) { - if ($package instanceof AliasPackage) { - $localAliasMap[$package->getName().'::'.$package->getVersion()] = $package; - $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package; - } else { - $localPackageMap[$package->getName()] = $package; - $removeMap[$package->getName()] = $package; - } - } - - $stack = $this->getRootPackages(); - - $visited = array(); - $processed = array(); - - while (!empty($stack)) { - $package = array_pop($stack); - - if (isset($processed[$package->id])) { - continue; - } - - if (!isset($visited[$package->id])) { - $visited[$package->id] = true; - - $stack[] = $package; - if ($package instanceof AliasPackage) { - $stack[] = $package->getAliasOf(); - } else { - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->getLockedProviders($link); - - foreach ($possibleRequires as $require) { - $stack[] = $require; - } - } - } - } elseif (!isset($processed[$package->id])) { - $processed[$package->id] = true; - - if ($package instanceof AliasPackage) { - $aliasKey = $package->getName().'::'.$package->getVersion(); - if (isset($localAliasMap[$aliasKey])) { - unset($removeAliasMap[$aliasKey]); - } else { - $operations[] = new Operation\MarkAliasInstalledOperation($package); - } - } else { - if (isset($localPackageMap[$package->getName()])) { - $source = $localPackageMap[$package->getName()]; - - // do we need to update? - if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { - $operations[] = new Operation\UpdateOperation($source, $package); - } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) { - $operations[] = new Operation\UpdateOperation($source, $package); - } - unset($removeMap[$package->getName()]); - } else { - $operations[] = new Operation\InstallOperation($package); - unset($removeMap[$package->getName()]); - } - } - } - } - - foreach ($removeMap as $name => $package) { - array_unshift($operations, new Operation\UninstallOperation($package, null)); - } - foreach ($removeAliasMap as $nameVersion => $package) { - $operations[] = new Operation\MarkAliasUninstalledOperation($package, null); - } - - $operations = $this->movePluginsToFront($operations); - // TODO fix this: - // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls - $operations = $this->moveUninstallsToFront($operations); - - // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? - /* - if ('update' === $jobType) { - $targetPackage = $operation->getTargetPackage(); - if ($targetPackage->isDev()) { - $initialPackage = $operation->getInitialPackage(); - if ($targetPackage->getVersion() === $initialPackage->getVersion() - && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) - && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) - ) { - $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); - $this->io->writeError('', true, IOInterface::DEBUG); - - continue; - } - } - }*/ - - return $operations; - } - - /** - * Determine which packages in the lock file are not required by any other packages in the lock file. - * - * These serve as a starting point to enumerate packages in a topological order despite potential cycles. - * If there are packages with a cycle on the top level the package with the lowest name gets picked - * - * @return array - */ - private function getRootPackages() - { - $roots = $this->lockedPackages; - - foreach ($this->lockedPackages as $packageId => $package) { - if (!isset($roots[$packageId])) { - continue; - } - - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->getLockedProviders($link); - - foreach ($possibleRequires as $require) { - if ($require !== $package) { - unset($roots[$require->id]); - } - } - } - } - - return $roots; - } - - private function getLockedProviders(Link $link) - { - if (!isset($this->lockedPackagesByName[$link->getTarget()])) { - return array(); - } - return $this->lockedPackagesByName[$link->getTarget()]; - } - - /** - * Workaround: if your packages depend on plugins, we must be sure - * that those are installed / updated first; else it would lead to packages - * being installed multiple times in different folders, when running Composer - * twice. - * - * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, - * it at least fixes the symptoms and makes usage of composer possible (again) - * in such scenarios. - * - * @param Operation\OperationInterface[] $operations - * @return Operation\OperationInterface[] reordered operation list - */ - private function movePluginsToFront(array $operations) - { - $pluginsNoDeps = array(); - $pluginsWithDeps = array(); - $pluginRequires = array(); - - foreach (array_reverse($operations, true) as $idx => $op) { - if ($op instanceof Operation\InstallOperation) { - $package = $op->getPackage(); - } elseif ($op instanceof Operation\UpdateOperation) { - $package = $op->getTargetPackage(); - } else { - continue; - } - - // is this package a plugin? - $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; - - // is this a plugin or a dependency of a plugin? - if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { - // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' - $requires = array_filter(array_keys($package->getRequires()), function ($req) { - return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); - }); - - // is this a plugin with no meaningful dependencies? - if ($isPlugin && !count($requires)) { - // plugins with no dependencies go to the very front - array_unshift($pluginsNoDeps, $op); - } else { - // capture the requirements for this package so those packages will be moved up as well - $pluginRequires = array_merge($pluginRequires, $requires); - // move the operation to the front - array_unshift($pluginsWithDeps, $op); - } - - unset($operations[$idx]); - } - } - - return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); - } - - /** - * Removals of packages should be executed before installations in - * case two packages resolve to the same path (due to custom installers) - * - * @param Operation\OperationInterface[] $operations - * @return Operation\OperationInterface[] reordered operation list - */ - private function moveUninstallsToFront(array $operations) - { - $uninstOps = array(); - foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation) { - $uninstOps[] = $op; - unset($operations[$idx]); - } - } - - return array_merge($uninstOps, $operations); + parent::__construct( + $localRepository->getPackages(), + $lockedRepository->getPackages() + ); } } diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 76468ddc6..b74cfbc78 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -23,12 +23,8 @@ use Composer\Test\Repository\ArrayRepositoryTest; /** * @author Nils Adermann */ -class LockTransaction +class LockTransaction extends Transaction { - protected $policy; - /** @var Pool */ - protected $pool; - /** * packages in current lock file, platform repo or otherwise present * @var array @@ -40,107 +36,32 @@ class LockTransaction * @var array */ protected $unlockableMap; - protected $decisions; - protected $resultPackages; /** * @var array */ - protected $operations; + protected $resultPackages; - public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions) + public function __construct(Pool $pool, $presentMap, $unlockableMap, $decisions) { - $this->policy = $policy; - $this->pool = $pool; $this->presentMap = $presentMap; $this->unlockableMap = $unlockableMap; - $this->decisions = $decisions; - $this->operations = $this->calculateOperations(); - } + $this->setResultPackages($pool, $decisions); + parent::__construct($this->presentMap, $this->resultPackages['all']); - /** - * @return OperationInterface[] - */ - public function getOperations() - { - return $this->operations; - } - - protected function calculateOperations() - { - $operations = array(); - $ignoreRemove = array(); - $lockMeansUpdateMap = $this->findPotentialUpdates(); - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - - $package = $this->pool->literalToPackage($literal); - - // wanted & !present - if ($literal > 0 && !isset($this->presentMap[spl_object_hash($package)])) { - if (isset($lockMeansUpdateMap[spl_object_hash($package)]) && !$package instanceof AliasPackage) { - // TODO we end up here sometimes because we prefer the remote package now to get up to date metadata - // TODO define some level of identity here for what constitutes an update and what can be ignored? new kind of metadata only update? - $target = $lockMeansUpdateMap[spl_object_hash($package)]; - if ($package->getName() !== $target->getName() || $package->getVersion() !== $target->getVersion()) { - $operations[] = new Operation\UpdateOperation($target, $package, $reason); - } - - // avoid updates to one package from multiple origins - $ignoreRemove[spl_object_hash($lockMeansUpdateMap[spl_object_hash($package)])] = true; - unset($lockMeansUpdateMap[spl_object_hash($package)]); - } else { - if ($package instanceof AliasPackage) { - $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason); - } else { - $operations[] = new Operation\InstallOperation($package, $reason); - } - } - } - } - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - $package = $this->pool->literalToPackage($literal); - - if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[spl_object_hash($package)])) { - if ($package instanceof AliasPackage) { - $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason); - } else { - $operations[] = new Operation\UninstallOperation($package, $reason); - } - } - } - - foreach ($this->presentMap as $package) { - if ($package->id === -1 && !isset($ignoreRemove[spl_object_hash($package)])) { - // TODO pass reason parameter to these two operations? - if ($package instanceof AliasPackage) { - $operations[] = new Operation\MarkAliasUninstalledOperation($package); - } else { - $operations[] = new Operation\UninstallOperation($package); - } - } - } - - $this->setResultPackages(); - - return $operations; } // TODO make this a bit prettier instead of the two text indexes? - public function setResultPackages() + public function setResultPackages(Pool $pool, Decisions $decisions) { - $this->resultPackages = array('non-dev' => array(), 'dev' => array()); - foreach ($this->decisions as $i => $decision) { + $this->resultPackages = array('all' => array(), 'non-dev' => array(), 'dev' => array()); + foreach ($decisions as $i => $decision) { $literal = $decision[Decisions::DECISION_LITERAL]; if ($literal > 0) { - $package = $this->pool->literalToPackage($literal); + $package = $pool->literalToPackage($literal); + $this->resultPackages['all'][] = $package; if (!isset($this->unlockableMap[$package->id])) { $this->resultPackages['non-dev'][] = $package; } @@ -191,46 +112,4 @@ class LockTransaction return $packages; } - - protected function findPotentialUpdates() - { - $lockMeansUpdateMap = array(); - - $packages = array(); - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $package = $this->pool->literalToPackage($literal); - - if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)])) { - $packages[spl_object_hash($package)] = $package; - } - } - - // some locked packages are not in the pool and thus, were not decided at all - foreach ($this->presentMap as $package) { - if ($package->id === -1) { - $packages[spl_object_hash($package)] = $package; - } - } - - foreach ($packages as $package) { - if ($package instanceof AliasPackage) { - continue; - } - - // TODO can't we just look at existing rules? - $updates = $this->policy->findUpdatePackages($this->pool, $package); - - $updatesAndPackage = array_merge(array($package), $updates); - - foreach ($updatesAndPackage as $update) { - if (!isset($lockMeansUpdateMap[spl_object_hash($update)])) { - $lockMeansUpdateMap[spl_object_hash($update)] = $package; - } - } - } - - return $lockMeansUpdateMap; - } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 66f325446..ad635ff00 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -221,7 +221,7 @@ class Solver throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool); } - return new LockTransaction($this->policy, $this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); + return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); } /** diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php new file mode 100644 index 000000000..f12aeccc5 --- /dev/null +++ b/src/Composer/DependencyResolver/Transaction.php @@ -0,0 +1,310 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; +use Composer\Package\AliasPackage; +use Composer\Package\Link; +use Composer\Package\PackageInterface; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Semver\Constraint\Constraint; + +/** + * @author Nils Adermann + */ +abstract class Transaction +{ + /** + * @var array + */ + protected $operations; + + /** + * Packages present at the beginning of the transaction + * @var array + */ + protected $presentPackages; + + /** + * Package set resulting from this transaction + * @var array + */ + protected $resultPackageMap; + + /** + * @var array + */ + protected $resultPackagesByName = array(); + + public function __construct($presentPackages, $resultPackages) + { + $this->presentPackages = $presentPackages; + $this->setResultPackageMaps($resultPackages); + $this->operations = $this->calculateOperations(); + + } + + public function getOperations() + { + return $this->operations; + } + + private function setResultPackageMaps($resultPackages) + { + $packageSort = function (PackageInterface $a, PackageInterface $b) { + // sort alias packages by the same name behind their non alias version + if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) { + return $a instanceof AliasPackage ? -1 : 1; + } + return strcmp($b->getName(), $a->getName()); + }; + + $this->resultPackageMap = array(); + foreach ($resultPackages as $package) { + $this->resultPackageMap[spl_object_hash($package)] = $package; + foreach ($package->getNames() as $name) { + $this->resultPackagesByName[$name][] = $package; + } + } + + uasort($this->resultPackageMap, $packageSort); + foreach ($this->resultPackagesByName as $name => $packages) { + uasort($this->resultPackagesByName[$name], $packageSort); + } + } + + protected function calculateOperations() + { + $operations = array(); + + $presentPackageMap = array(); + $removeMap = array(); + $presentAliasMap = array(); + $removeAliasMap = array(); + foreach ($this->presentPackages as $package) { + if ($package instanceof AliasPackage) { + $presentAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + } else { + $presentPackageMap[$package->getName()] = $package; + $removeMap[$package->getName()] = $package; + } + } + + $stack = $this->getRootPackages(); + + $visited = array(); + $processed = array(); + + while (!empty($stack)) { + $package = array_pop($stack); + + if (isset($processed[spl_object_hash($package)])) { + continue; + } + + if (!isset($visited[spl_object_hash($package)])) { + $visited[spl_object_hash($package)] = true; + + $stack[] = $package; + if ($package instanceof AliasPackage) { + $stack[] = $package->getAliasOf(); + } else { + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->getProvidersInResult($link); + + foreach ($possibleRequires as $require) { + $stack[] = $require; + } + } + } + } elseif (!isset($processed[spl_object_hash($package)])) { + $processed[spl_object_hash($package)] = true; + + if ($package instanceof AliasPackage) { + $aliasKey = $package->getName().'::'.$package->getVersion(); + if (isset($presentAliasMap[$aliasKey])) { + unset($removeAliasMap[$aliasKey]); + } else { + $operations[] = new Operation\MarkAliasInstalledOperation($package); + } + } else { + if (isset($presentPackageMap[$package->getName()])) { + $source = $presentPackageMap[$package->getName()]; + + // do we need to update? + // TODO different for lock? + if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } elseif ($package->isDev() && $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } + unset($removeMap[$package->getName()]); + } else { + $operations[] = new Operation\InstallOperation($package); + unset($removeMap[$package->getName()]); + } + } + } + } + + foreach ($removeMap as $name => $package) { + array_unshift($operations, new Operation\UninstallOperation($package, null)); + } + foreach ($removeAliasMap as $nameVersion => $package) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package, null); + } + + $operations = $this->movePluginsToFront($operations); + // TODO fix this: + // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls + $operations = $this->moveUninstallsToFront($operations); + + // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? + /* + if ('update' === $jobType) { + $targetPackage = $operation->getTargetPackage(); + if ($targetPackage->isDev()) { + $initialPackage = $operation->getInitialPackage(); + if ($targetPackage->getVersion() === $initialPackage->getVersion() + && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) + && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) + ) { + $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); + $this->io->writeError('', true, IOInterface::DEBUG); + + continue; + } + } + }*/ + + return $this->operations = $operations; + } + + /** + * Determine which packages in the result are not required by any other packages in it. + * + * These serve as a starting point to enumerate packages in a topological order despite potential cycles. + * If there are packages with a cycle on the top level the package with the lowest name gets picked + * + * @return array + */ + protected function getRootPackages() + { + $roots = $this->resultPackageMap; + + foreach ($this->resultPackageMap as $packageHash => $package) { + if (!isset($roots[$packageHash])) { + continue; + } + + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->getProvidersInResult($link); + + foreach ($possibleRequires as $require) { + if ($require !== $package) { + unset($roots[spl_object_hash($require)]); + } + } + } + } + + return $roots; + } + + protected function getProvidersInResult(Link $link) + { + if (!isset($this->resultPackagesByName[$link->getTarget()])) { + return array(); + } + return $this->resultPackagesByName[$link->getTarget()]; + } + + /** + * Workaround: if your packages depend on plugins, we must be sure + * that those are installed / updated first; else it would lead to packages + * being installed multiple times in different folders, when running Composer + * twice. + * + * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, + * it at least fixes the symptoms and makes usage of composer possible (again) + * in such scenarios. + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function movePluginsToFront(array $operations) + { + $pluginsNoDeps = array(); + $pluginsWithDeps = array(); + $pluginRequires = array(); + + foreach (array_reverse($operations, true) as $idx => $op) { + if ($op instanceof Operation\InstallOperation) { + $package = $op->getPackage(); + } elseif ($op instanceof Operation\UpdateOperation) { + $package = $op->getTargetPackage(); + } else { + continue; + } + + // is this package a plugin? + $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; + + // is this a plugin or a dependency of a plugin? + if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { + // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' + $requires = array_filter(array_keys($package->getRequires()), function ($req) { + return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); + + // is this a plugin with no meaningful dependencies? + if ($isPlugin && !count($requires)) { + // plugins with no dependencies go to the very front + array_unshift($pluginsNoDeps, $op); + } else { + // capture the requirements for this package so those packages will be moved up as well + $pluginRequires = array_merge($pluginRequires, $requires); + // move the operation to the front + array_unshift($pluginsWithDeps, $op); + } + + unset($operations[$idx]); + } + } + + return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); + } + + /** + * Removals of packages should be executed before installations in + * case two packages resolve to the same path (due to custom installers) + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function moveUninstallsToFront(array $operations) + { + $uninstOps = array(); + foreach ($operations as $idx => $op) { + if ($op instanceof UninstallOperation) { + $uninstOps[] = $op; + unset($operations[$idx]); + } + } + + return array_merge($uninstOps, $operations); + } +} diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8b5c85057..941717912 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -486,23 +486,6 @@ class Installer if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation); } - - // output reasons why the operation was run, only for install/update operations - if ($this->verbose && $this->io->isVeryVerbose() && in_array($jobType, array('install', 'update'))) { - $reason = $operation->getReason(); - if ($reason instanceof Rule) { - switch ($reason->getReason()) { - case Rule::RULE_JOB_INSTALL: - $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - case Rule::RULE_PACKAGE_REQUIRES: - $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - } - } - } } $updatedLock = $this->locker->setLockData( diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 8f12f3122..4c733a4fb 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -168,9 +168,9 @@ class SolverTest extends TestCase $this->request->install('C'); $this->checkSolverResult(array( + array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageA), )); } @@ -338,15 +338,15 @@ class SolverTest extends TestCase $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array( + array( + 'job' => 'remove', + 'package' => $packageB, + ), array( 'job' => 'update', 'from' => $packageA, 'to' => $newPackageA, ), - array( - 'job' => 'remove', - 'package' => $packageB, - ), )); } @@ -369,10 +369,10 @@ class SolverTest extends TestCase $this->request->remove('D'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageB), - array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), - array('job' => 'install', 'package' => $packageA), array('job' => 'remove', 'package' => $packageD), + array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA), + array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), )); } @@ -406,7 +406,8 @@ class SolverTest extends TestCase $this->request->install('B'); $this->checkSolverResult(array( - array('job' => 'update', 'from' => $packageA, 'to' => $packageB), + array('job' => 'remove', 'package' => $packageA), + array('job' => 'install', 'package' => $packageB), )); } @@ -526,8 +527,8 @@ class SolverTest extends TestCase $this->request->install('X'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $newPackageB), + array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageX), )); } @@ -571,8 +572,8 @@ class SolverTest extends TestCase $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageA), + array('job' => 'install', 'package' => $packageC), )); } @@ -829,9 +830,9 @@ class SolverTest extends TestCase $this->request->install('B'); $this->checkSolverResult(array( + array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageAAlias), array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageA), )); } @@ -894,12 +895,12 @@ class SolverTest extends TestCase $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageC2), - array('job' => 'install', 'package' => $packageG2), array('job' => 'install', 'package' => $packageF1), + array('job' => 'install', 'package' => $packageD), + array('job' => 'install', 'package' => $packageG2), + array('job' => 'install', 'package' => $packageC2), array('job' => 'install', 'package' => $packageE), array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageD), array('job' => 'install', 'package' => $packageA), )); diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index cdf648c0d..7c671565d 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -27,8 +27,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing c/c (1.0.0) - Installing a/a (1.0.0) + - Installing c/c (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index 878e9429a..e7730cefc 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -55,8 +55,8 @@ update a/a b/b --with-dependencies Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 2 updates, 0 removals - - Updating b/b (1.0.0) to b/b (1.1.0) - Updating a/a (1.0.0) to a/a (1.1.0) + - Updating b/b (1.0.0) to b/b (1.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index 6995f78e7..e53ab0065 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -22,8 +22,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing b/b (1.0.0) - Installing a/a (1.0.0) + - Installing b/b (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index 14d8e13d8..8a72cec66 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -50,8 +50,8 @@ update b/b --with-all-dependencies Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 2 updates, 0 removals - - Updating b/b (1.0.0) to b/b (1.1.0) - Updating a/a (1.0.0) to a/a (1.1.0) + - Updating b/b (1.0.0) to b/b (1.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals From 3cbe91983c63e0231d455abbf76632a80db74678 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 16:48:35 +0100 Subject: [PATCH 136/321] Display Locking instead of Installing for lock file install operations --- .../DependencyResolver/Operation/InstallOperation.php | 10 +++++++++- .../Operation/MarkAliasInstalledOperation.php | 10 +++++++++- .../Operation/MarkAliasUninstalledOperation.php | 10 +++++++++- .../Operation/OperationInterface.php | 8 ++++++++ .../DependencyResolver/Operation/SolverOperation.php | 6 ++++++ .../Operation/UninstallOperation.php | 10 +++++++++- .../DependencyResolver/Operation/UpdateOperation.php | 10 +++++++++- src/Composer/DependencyResolver/Transaction.php | 3 +-- src/Composer/Installer.php | 4 ++-- .../Test/Fixtures/installer/abandoned-listed.test | 4 ++-- .../Test/Fixtures/installer/suggest-installed.test | 4 ++-- .../Composer/Test/Fixtures/installer/suggest-prod.test | 2 +- .../Test/Fixtures/installer/suggest-replaced.test | 4 ++-- .../Test/Fixtures/installer/suggest-uninstalled.test | 2 +- 14 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/Composer/DependencyResolver/Operation/InstallOperation.php b/src/Composer/DependencyResolver/Operation/InstallOperation.php index 08c659c49..569ae67a4 100644 --- a/src/Composer/DependencyResolver/Operation/InstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/InstallOperation.php @@ -56,11 +56,19 @@ class InstallOperation extends SolverOperation return 'install'; } + /** + * {@inheritDoc} + */ + public function show($lock) + { + return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; + } + /** * {@inheritDoc} */ public function __toString() { - return 'Installing '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; + return $this->show(false); } } diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php index 920e54e67..800cf43c2 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php @@ -60,8 +60,16 @@ class MarkAliasInstalledOperation extends SolverOperation /** * {@inheritDoc} */ - public function __toString() + public function show($lock) { return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')'; } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->show(false); + } } diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php index 77f3aef8c..3c996f343 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php @@ -60,8 +60,16 @@ class MarkAliasUninstalledOperation extends SolverOperation /** * {@inheritDoc} */ - public function __toString() + public function show($lock) { return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')'; } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->show(false); + } } diff --git a/src/Composer/DependencyResolver/Operation/OperationInterface.php b/src/Composer/DependencyResolver/Operation/OperationInterface.php index 330cbceb1..261a746af 100644 --- a/src/Composer/DependencyResolver/Operation/OperationInterface.php +++ b/src/Composer/DependencyResolver/Operation/OperationInterface.php @@ -33,6 +33,14 @@ interface OperationInterface */ public function getReason(); + /** + * Serializes the operation in a human readable format + * + * @param $lock bool Whether this is an operation on the lock file + * @return string + */ + public function show($lock); + /** * Serializes the operation in a human readable format * diff --git a/src/Composer/DependencyResolver/Operation/SolverOperation.php b/src/Composer/DependencyResolver/Operation/SolverOperation.php index e1a68585e..bb733b70f 100644 --- a/src/Composer/DependencyResolver/Operation/SolverOperation.php +++ b/src/Composer/DependencyResolver/Operation/SolverOperation.php @@ -43,6 +43,12 @@ abstract class SolverOperation implements OperationInterface return $this->reason; } + /** + * @param $lock bool Whether this is an operation on the lock file + * @return string + */ + abstract public function show($lock); + protected function formatVersion(PackageInterface $package) { return $package->getFullPrettyVersion(); diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index b4a73811e..73321f96f 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -59,8 +59,16 @@ class UninstallOperation extends SolverOperation /** * {@inheritDoc} */ - public function __toString() + public function show($lock) { return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->show(false); + } } diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index 836725ef5..aecde33c2 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -72,9 +72,17 @@ class UpdateOperation extends SolverOperation /** * {@inheritDoc} */ - public function __toString() + public function show($lock) { return 'Updating '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).') to '. $this->targetPackage->getPrettyName(). ' ('.$this->formatVersion($this->targetPackage).')'; } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->show(false); + } } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index f12aeccc5..6496327ff 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -24,7 +24,7 @@ use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann */ -abstract class Transaction +class Transaction { /** * @var array @@ -53,7 +53,6 @@ abstract class Transaction $this->presentPackages = $presentPackages; $this->setResultPackageMaps($resultPackages); $this->operations = $this->calculateOperations(); - } public function getOperations() diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 941717912..9f014be1e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -484,7 +484,7 @@ class Installer // output op, but alias op only in debug verbosity if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { - $this->io->writeError(' - ' . $operation); + $this->io->writeError(' - ' . $operation->show(true)); } } @@ -673,7 +673,7 @@ class Installer // output op, but alias op only in debug verbosity if ((!$this->executeOperations && false === strpos($operation->getJobType(), 'Alias')) || $this->io->isDebug()) { - $this->io->writeError(' - ' . $operation); + $this->io->writeError(' - ' . $operation->show(false)); } $this->installationManager->execute($localRepo, $operation); diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index 7c671565d..3ad5b0428 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -27,8 +27,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing a/a (1.0.0) - - Installing c/c (1.0.0) + - Locking a/a (1.0.0) + - Locking c/c (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index e53ab0065..413ac8665 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -22,8 +22,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing a/a (1.0.0) - - Installing b/b (1.0.0) + - Locking a/a (1.0.0) + - Locking b/b (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index 89d6ab8de..6a00b5607 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -21,7 +21,7 @@ install --no-dev Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - - Installing a/a (1.0.0) + - Locking a/a (1.0.0) Writing lock file Installing dependencies from lock file Package operations: 1 install, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index 62c13e560..e56af5093 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -22,8 +22,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing c/c (1.0.0) - - Installing a/a (1.0.0) + - Locking c/c (1.0.0) + - Locking a/a (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index f45b710f0..1b88b2d8b 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -21,7 +21,7 @@ install Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - - Installing a/a (1.0.0) + - Locking a/a (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals From aa6bc75af2f041254f4b159eb60091cd90ab66eb Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 17:51:18 +0100 Subject: [PATCH 137/321] Add a test case for transactions to verify correct sorting --- .../DependencyResolver/Transaction.php | 6 +- .../DependencyResolver/TransactionTest.php | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/Composer/Test/DependencyResolver/TransactionTest.php diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index 6496327ff..04e2e966d 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -12,14 +12,10 @@ namespace Composer\DependencyResolver; -use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; -use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\Package\AliasPackage; use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositoryInterface; -use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann @@ -298,7 +294,7 @@ class Transaction { $uninstOps = array(); foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation) { + if ($op instanceof Operation\UninstallOperation) { $uninstOps[] = $op; unset($operations[$idx]); } diff --git a/tests/Composer/Test/DependencyResolver/TransactionTest.php b/tests/Composer/Test/DependencyResolver/TransactionTest.php new file mode 100644 index 000000000..6cde5d5d0 --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/TransactionTest.php @@ -0,0 +1,90 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\DependencyResolver; + +use Composer\DependencyResolver\Transaction; +use Composer\Package\Link; +use Composer\Test\TestCase; + +class TransactionTest extends TestCase +{ + public function setUp() + { + } + + public function testTransactionGenerationAndSorting() + { + $presentPackages = array( + $packageA = $this->getPackage('a/a', 'dev-master'), + $packageAalias = $this->getAliasPackage($packageA, '1.0.x-dev'), + $packageB = $this->getPackage('b/b', '1.0.0'), + $packageE = $this->getPackage('e/e', 'dev-foo'), + $packageEalias = $this->getAliasPackage($packageE, '1.0.x-dev'), + $packageC = $this->getPackage('c/c', '1.0.0'), + ); + $resultPackages = array( + $packageA, + $packageAalias, + $packageBnew = $this->getPackage('b/b', '2.1.3'), + $packageD = $this->getPackage('d/d', '1.2.3'), + $packageF = $this->getPackage('f/f', '1.0.0'), + $packageFalias1 = $this->getAliasPackage($packageF, 'dev-foo'), + $packageG = $this->getPackage('g/g', '1.0.0'), + $packageA0first = $this->getPackage('a0/first', '1.2.3'), + $packageFalias2 = $this->getAliasPackage($packageF, 'dev-bar'), + ); + + $packageD->setRequires(array( + 'f/f' => new Link('d/d', 'f/f', $this->getVersionConstraint('>', '0.2'), 'requires'), + 'g/provider' => new Link('d/d', 'g/provider', $this->getVersionConstraint('>', '0.2'), 'requires'), + )); + $packageG->setProvides(array('g/provider' => new Link('g/g', 'g/provider', $this->getVersionConstraint('==', '1.0.0'), 'provides'))); + + $expectedOperations = array( + array('job' => 'uninstall', 'package' => $packageC), + array('job' => 'uninstall', 'package' => $packageE), + array('job' => 'install', 'package' => $packageA0first), + array('job' => 'update', 'from' => $packageB, 'to' => $packageBnew), + array('job' => 'install', 'package' => $packageG), + array('job' => 'install', 'package' => $packageF), + array('job' => 'markAliasInstalled', 'package' => $packageFalias1), + array('job' => 'markAliasInstalled', 'package' => $packageFalias2), + array('job' => 'install', 'package' => $packageD), + array('job' => 'markAliasUninstalled', 'package' => $packageEalias), + ); + + $transaction = new Transaction($presentPackages, $resultPackages); + $this->checkTransactionOperations($transaction, $expectedOperations); + } + + protected function checkTransactionOperations(Transaction $transaction, array $expected) + { + $result = array(); + foreach ($transaction->getOperations() as $operation) { + if ('update' === $operation->getJobType()) { + $result[] = array( + 'job' => 'update', + 'from' => $operation->getInitialPackage(), + 'to' => $operation->getTargetPackage(), + ); + } else { + $result[] = array( + 'job' => $operation->getJobType(), + 'package' => $operation->getPackage(), + ); + } + } + + $this->assertEquals($expected, $result); + } +} From 7ef3a31de7907b131c627d9dcdd60ad1f3f6751c Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 12 Nov 2019 22:00:02 +0100 Subject: [PATCH 138/321] Make TransportException in network disabled case clearer Otherwise it's a bit hard to work out what request triggered the exception --- src/Composer/Util/HttpDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 93026ecbe..c4b6c5424 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -229,7 +229,7 @@ class HttpDownloader if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) { $resolve(new Response(array('url' => $url), 304, array(), '')); } else { - $e = new TransportException('Network disabled', 499); + $e = new TransportException('Network disabled, request canceled: '.$url, 499); $e->setStatusCode(499); $reject($e); } From c3251126701d4b0ea161a3dd20d55b746dd6da6b Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 12 Nov 2019 22:22:03 +0100 Subject: [PATCH 139/321] When network is disabled all uncached requests result in 404 --- src/Composer/Util/HttpDownloader.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index c4b6c5424..fcd68d25c 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -229,9 +229,7 @@ class HttpDownloader if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) { $resolve(new Response(array('url' => $url), 304, array(), '')); } else { - $e = new TransportException('Network disabled, request canceled: '.$url, 499); - $e->setStatusCode(499); - $reject($e); + $resolve(new Response(array('url' => $url), 404, array(), '')); } return; } From 6f9b1e76e35f7721dfc0c1d81dbb8eeee069941b Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 12 Nov 2019 22:53:52 +0100 Subject: [PATCH 140/321] Remove disableRules code from Solver, leftover from original C code This goes back to an input option to install recommended packages, which would in turn allow removal of these packages if that was needed to resolve the rest. This was supported in very early versions of Composer with suggested packages. We later realized this was not useful in the context of a project based dependency manager with a lock file, so it was removed but the solver was never cleaned up. --- src/Composer/DependencyResolver/Solver.php | 42 ++++++---------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index ad635ff00..08482a14f 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -213,7 +213,7 @@ class Solver $this->io->writeError('Resolving dependencies through SAT', true, IOInterface::DEBUG); $before = microtime(true); - $this->runSat(true); + $this->runSat(); $this->io->writeError('', true, IOInterface::DEBUG); $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); @@ -298,11 +298,10 @@ class Solver * * @param int $level * @param string|int $literal - * @param bool $disableRules * @param Rule $rule * @return int */ - private function setPropagateLearn($level, $literal, $disableRules, Rule $rule) + private function setPropagateLearn($level, $literal, Rule $rule) { $level++; @@ -316,7 +315,7 @@ class Solver } if ($level == 1) { - return $this->analyzeUnsolvable($rule, $disableRules); + return $this->analyzeUnsolvable($rule); } // conflict @@ -353,11 +352,10 @@ class Solver /** * @param int $level * @param array $decisionQueue - * @param bool $disableRules * @param Rule $rule * @return int */ - private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule) + private function selectAndInstall($level, array $decisionQueue, Rule $rule) { // choose best package to install from decisionQueue $literals = $this->policy->selectPreferredPackages($this->pool, $decisionQueue, $rule->getRequiredPackage()); @@ -369,7 +367,7 @@ class Solver $this->branches[] = array($literals, $level); } - return $this->setPropagateLearn($level, $selectedLiteral, $disableRules, $rule); + return $this->setPropagateLearn($level, $selectedLiteral, $rule); } /** @@ -515,10 +513,9 @@ class Solver /** * @param Rule $conflictRule - * @param bool $disableRules * @return int */ - private function analyzeUnsolvable(Rule $conflictRule, $disableRules) + private function analyzeUnsolvable(Rule $conflictRule) { $problem = new Problem($this->pool); $problem->addRule($conflictRule); @@ -562,16 +559,6 @@ class Solver } } - if ($disableRules) { - foreach ($this->problems[count($this->problems) - 1] as $reason) { - $this->disableProblem($reason['rule']); - } - - $this->resetSolver(); - - return 1; - } - return 0; } @@ -637,10 +624,7 @@ class Solver } } - /** - * @param bool $disableRules - */ - private function runSat($disableRules = true) + private function runSat() { $this->propagateIndex = 0; @@ -656,10 +640,6 @@ class Solver $decisionQueue = array(); $decisionSupplementQueue = array(); - /** - * @todo this makes $disableRules always false; determine the rationale and possibly remove dead code? - */ - $disableRules = false; $level = 1; $systemLevel = $level + 1; @@ -669,7 +649,7 @@ class Solver if (1 === $level) { $conflictRule = $this->propagate($level); if (null !== $conflictRule) { - if ($this->analyzeUnsolvable($conflictRule, $disableRules)) { + if ($this->analyzeUnsolvable($conflictRule)) { continue; } @@ -710,7 +690,7 @@ class Solver if ($noneSatisfied && count($decisionQueue)) { $oLevel = $level; - $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule); + $level = $this->selectAndInstall($level, $decisionQueue, $rule); if (0 === $level) { return; @@ -786,7 +766,7 @@ class Solver continue; } - $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule); + $level = $this->selectAndInstall($level, $decisionQueue, $rule); if (0 === $level) { return; @@ -829,7 +809,7 @@ class Solver $why = $this->decisions->lastReason(); - $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why); + $level = $this->setPropagateLearn($level, $lastLiteral, $why); if ($level == 0) { return; From ed300b9f22268e08b7131a20997277db875cd7a5 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 12 Dec 2018 02:33:44 +0100 Subject: [PATCH 141/321] New Multi Conflict Rule for transitive conflicts, to reduce memory --- .../DependencyResolver/MultiConflictRule.php | 99 +++++++++++++++++++ .../DependencyResolver/RuleSetGenerator.php | 32 +++++- .../DependencyResolver/RuleWatchGraph.php | 66 +++++++++---- .../DependencyResolver/RuleWatchNode.php | 2 +- 4 files changed, 174 insertions(+), 25 deletions(-) create mode 100644 src/Composer/DependencyResolver/MultiConflictRule.php diff --git a/src/Composer/DependencyResolver/MultiConflictRule.php b/src/Composer/DependencyResolver/MultiConflictRule.php new file mode 100644 index 000000000..8ec6b46c0 --- /dev/null +++ b/src/Composer/DependencyResolver/MultiConflictRule.php @@ -0,0 +1,99 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\Package\PackageInterface; +use Composer\Package\Link; + +/** + * @author Nils Adermann + * + * MultiConflictRule([A, B, C]) acts as Rule([-A, -B]), Rule([-A, -C]), Rule([-B, -C]) + */ +class MultiConflictRule extends Rule +{ + protected $literals; + + /** + * @param array $literals + * @param int $reason A RULE_* constant describing the reason for generating this rule + * @param Link|PackageInterface $reasonData + * @param array $job The job this rule was created from + */ + public function __construct(array $literals, $reason, $reasonData, $job = null) + { + parent::__construct($reason, $reasonData, $job); + + // sort all packages ascending by id + sort($literals); + + $this->literals = $literals; + } + + public function getLiterals() + { + return $this->literals; + } + + public function getHash() + { + $data = unpack('ihash', md5('c:'.implode(',', $this->literals), true)); + + return $data['hash']; + } + + /** + * Checks if this rule is equal to another one + * + * Ignores whether either of the rules is disabled. + * + * @param Rule $rule The rule to check against + * @return bool Whether the rules are equal + */ + public function equals(Rule $rule) + { + return $this->literals === $rule->getLiterals(); + } + + public function isAssertion() + { + return false; + } + + public function disable() + { + throw new \RuntimeException("can't disable conflict rule"); + } + + /** + * Formats a rule as a string of the format (Literal1|Literal2|...) + * + * @return string + */ + public function __toString() + { + // TODO multi conflict? + $result = $this->isDisabled() ? 'disabled(multi(' : '(multi('; + + foreach ($this->literals as $i => $literal) { + if ($i != 0) { + $result .= '|'; + } + $result .= $literal; + } + + $result .= '))'; + + return $result; + } +} diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 76f9270be..aaa253bed 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -30,6 +30,7 @@ class RuleSetGenerator protected $conflictAddedMap; protected $addedPackages; protected $addedPackagesByNames; + protected $conflictsForName; public function __construct(PolicyInterface $policy, Pool $pool) { @@ -128,6 +129,16 @@ class RuleSetGenerator return new Rule2Literals(-$issuer->id, -$provider->id, $reason, $reasonData); } + protected function createMultiConflictRule(array $packages, $reason, $reasonData = null) + { + $literals = array(); + foreach ($packages as $package) { + $literals[] = -$package->id; + } + + return new MultiConflictRule($literals, $reason, $reasonData); + } + /** * Adds a rule unless it duplicates an existing one of any type * @@ -189,9 +200,16 @@ class RuleSetGenerator if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package)); - } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) { - $reason = ($packageName == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES; - $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $package)); + } else { + if (!isset($this->conflictsForName[$packageName])) { + $this->conflictsForName[$packageName] = array(); + } + if (!$package instanceof AliasPackage) { + $this->conflictsForName[$packageName][$package->id] = $package; + } + if (!$provider instanceof AliasPackage) { + $this->conflictsForName[$packageName][$provider->id] = $provider; + } } } } @@ -240,6 +258,13 @@ class RuleSetGenerator } } } + + foreach ($this->conflictsForName as $name => $packages) { + if (count($packages) > 1) { + $reason = Rule::RULE_PACKAGE_SAME_NAME; + $this->addRule(RuleSet::TYPE_PACKAGE, $this->createMultiConflictRule($packages, $reason, null)); + } + } } protected function obsoleteImpossibleForAlias($package, $provider) @@ -316,6 +341,7 @@ class RuleSetGenerator $this->conflictAddedMap = array(); $this->addedPackages = array(); $this->addedPackagesByNames = array(); + $this->conflictsForName = array(); $this->addRulesForRequest($request, $ignorePlatformReqs); diff --git a/src/Composer/DependencyResolver/RuleWatchGraph.php b/src/Composer/DependencyResolver/RuleWatchGraph.php index 31a22414d..59a259356 100644 --- a/src/Composer/DependencyResolver/RuleWatchGraph.php +++ b/src/Composer/DependencyResolver/RuleWatchGraph.php @@ -44,13 +44,24 @@ class RuleWatchGraph return; } - foreach (array($node->watch1, $node->watch2) as $literal) { - if (!isset($this->watchChains[$literal])) { - $this->watchChains[$literal] = new RuleWatchChain; - } + if (!$node->getRule() instanceof MultiConflictRule) { + foreach (array($node->watch1, $node->watch2) as $literal) { + if (!isset($this->watchChains[$literal])) { + $this->watchChains[$literal] = new RuleWatchChain; + } - $this->watchChains[$literal]->unshift($node); + $this->watchChains[$literal]->unshift($node); + } + } else { + foreach ($node->getRule()->getLiterals() as $literal) { + if (!isset($this->watchChains[$literal])) { + $this->watchChains[$literal] = new RuleWatchChain; + } + + $this->watchChains[$literal]->unshift($node); + } } + } /** @@ -92,28 +103,41 @@ class RuleWatchGraph $chain->rewind(); while ($chain->valid()) { $node = $chain->current(); - $otherWatch = $node->getOtherWatch($literal); + if (!$node->getRule() instanceof MultiConflictRule) { + $otherWatch = $node->getOtherWatch($literal); - if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) { - $ruleLiterals = $node->getRule()->getLiterals(); + if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) { + $ruleLiterals = $node->getRule()->getLiterals(); - $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) { - return $literal !== $ruleLiteral && - $otherWatch !== $ruleLiteral && - !$decisions->conflict($ruleLiteral); - }); + $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) { + return $literal !== $ruleLiteral && + $otherWatch !== $ruleLiteral && + !$decisions->conflict($ruleLiteral); + }); - if ($alternativeLiterals) { - reset($alternativeLiterals); - $this->moveWatch($literal, current($alternativeLiterals), $node); - continue; + if ($alternativeLiterals) { + reset($alternativeLiterals); + $this->moveWatch($literal, current($alternativeLiterals), $node); + continue; + } + + if ($decisions->conflict($otherWatch)) { + return $node->getRule(); + } + + $decisions->decide($otherWatch, $level, $node->getRule()); } + } else { + // check isDisabled? + foreach ($node->getRule()->getLiterals() as $otherLiteral) { + if ($literal !== $otherLiteral && !$decisions->satisfy($otherLiteral)) { + if ($decisions->conflict($otherLiteral)) { + return $node->getRule(); + } - if ($decisions->conflict($otherWatch)) { - return $node->getRule(); + $decisions->decide($otherLiteral, $level, $node->getRule()); + } } - - $decisions->decide($otherWatch, $level, $node->getRule()); } $chain->next(); diff --git a/src/Composer/DependencyResolver/RuleWatchNode.php b/src/Composer/DependencyResolver/RuleWatchNode.php index eeaa54162..926c144b4 100644 --- a/src/Composer/DependencyResolver/RuleWatchNode.php +++ b/src/Composer/DependencyResolver/RuleWatchNode.php @@ -55,7 +55,7 @@ class RuleWatchNode $literals = $this->rule->getLiterals(); // if there are only 2 elements, both are being watched anyway - if (count($literals) < 3) { + if (count($literals) < 3 || $this->rule instanceof MultiConflictRule) { return; } From dc0f2e7e462b19ca83b835f61dbe55063f30bdb0 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 12 Nov 2019 22:19:32 +0100 Subject: [PATCH 142/321] Ensure multi conflict rules are only used for 3+ literals Implements the equals method correctly on multi conflict rules. If there are fewer literals a regular Rule2Literals is enough to represent the basic conflict rule. --- src/Composer/DependencyResolver/MultiConflictRule.php | 9 ++++++++- src/Composer/DependencyResolver/RuleSetGenerator.php | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/MultiConflictRule.php b/src/Composer/DependencyResolver/MultiConflictRule.php index 8ec6b46c0..d564ced2e 100644 --- a/src/Composer/DependencyResolver/MultiConflictRule.php +++ b/src/Composer/DependencyResolver/MultiConflictRule.php @@ -34,6 +34,10 @@ class MultiConflictRule extends Rule { parent::__construct($reason, $reasonData, $job); + if (count($literals) < 3) { + throw new \RuntimeException("multi conflict rule requires at least 3 literals"); + } + // sort all packages ascending by id sort($literals); @@ -62,7 +66,10 @@ class MultiConflictRule extends Rule */ public function equals(Rule $rule) { - return $this->literals === $rule->getLiterals(); + if ($rule instanceof MultiConflictRule) { + return $this->literals === $rule->getLiterals(); + } + return false; } public function isAssertion() diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index aaa253bed..442be135c 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -136,6 +136,10 @@ class RuleSetGenerator $literals[] = -$package->id; } + if (count($literals) == 2) { + return new Rule2Literals($literals[0], $literals[1], $reason, $reasonData); + } + return new MultiConflictRule($literals, $reason, $reasonData); } From 79066931e699a8122ec35d7db03f053260a47891 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 12 Nov 2019 22:53:27 +0100 Subject: [PATCH 143/321] Update exception message for disabling multi conflict rules --- src/Composer/DependencyResolver/MultiConflictRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/MultiConflictRule.php b/src/Composer/DependencyResolver/MultiConflictRule.php index d564ced2e..f2c27c406 100644 --- a/src/Composer/DependencyResolver/MultiConflictRule.php +++ b/src/Composer/DependencyResolver/MultiConflictRule.php @@ -79,7 +79,7 @@ class MultiConflictRule extends Rule public function disable() { - throw new \RuntimeException("can't disable conflict rule"); + throw new \RuntimeException("Disabling multi conflict rules is not possible. Please contact composer at https://github.com/composer/composer to let us debug what lead to this situation."); } /** From 28afc4de32e5d795948403e031a0a297dcebc252 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 12 Nov 2019 23:04:58 +0100 Subject: [PATCH 144/321] MultiConflictRules cannot be disabled, so no need to check --- src/Composer/DependencyResolver/RuleWatchGraph.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Composer/DependencyResolver/RuleWatchGraph.php b/src/Composer/DependencyResolver/RuleWatchGraph.php index 59a259356..e7fae0c61 100644 --- a/src/Composer/DependencyResolver/RuleWatchGraph.php +++ b/src/Composer/DependencyResolver/RuleWatchGraph.php @@ -128,7 +128,6 @@ class RuleWatchGraph $decisions->decide($otherWatch, $level, $node->getRule()); } } else { - // check isDisabled? foreach ($node->getRule()->getLiterals() as $otherLiteral) { if ($literal !== $otherLiteral && !$decisions->satisfy($otherLiteral)) { if ($decisions->conflict($otherLiteral)) { From eb3e3063b85cf497c65779b008325760dc1207e2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 14 Nov 2019 09:55:57 +0100 Subject: [PATCH 145/321] Revert "When network is disabled all uncached requests result in 404" This reverts commit c3251126701d4b0ea161a3dd20d55b746dd6da6b. --- src/Composer/Util/HttpDownloader.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index fcd68d25c..c4b6c5424 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -229,7 +229,9 @@ class HttpDownloader if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) { $resolve(new Response(array('url' => $url), 304, array(), '')); } else { - $resolve(new Response(array('url' => $url), 404, array(), '')); + $e = new TransportException('Network disabled, request canceled: '.$url, 499); + $e->setStatusCode(499); + $reject($e); } return; } From 3b6d517ce0f44e6b6022029b45903ee3f0f9570b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 14 Nov 2019 09:57:59 +0100 Subject: [PATCH 146/321] Return 404s in ComposerRepository when network is disabled instead of failing hard --- src/Composer/Repository/ComposerRepository.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index a53b37c33..e4cfbba83 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -31,6 +31,7 @@ use Composer\Downloader\TransportException; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; +use Composer\Util\Http\Response; /** * @author Jordi Boggiano @@ -1140,6 +1141,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $degradedMode = true; + // special error code returned when network is being artificially disabled + if ($e instanceof TransportException && $e->getStatusCode() === 499) { + return $accept(new Response(array('url' => $url), 404, array(), '')); + } + throw $e; }; From 006985a0ea7135408d9760125a74d862c17409cb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 14 Nov 2019 15:20:50 +0100 Subject: [PATCH 147/321] Execute all operations at once which lets us download all packages in parallel then install only once all downloads succeeded, fixes #2847 This also changes the PRE/POST_PACKAGE_INSTALL/UPDATE/UNINSTALL events to have less information available on them, repositorySet, request and policy are gone --- src/Composer/Command/CreateProjectCommand.php | 2 +- .../EventDispatcher/EventDispatcher.php | 23 ++-- src/Composer/Factory.php | 6 +- src/Composer/Installer.php | 30 ++--- .../Installer/InstallationManager.php | 127 +++++++++++------- src/Composer/Installer/InstallerEvent.php | 18 +-- src/Composer/Installer/PackageEvent.php | 85 ++++++++++-- .../installer/update-all-dry-run.test | 13 +- .../Installer/InstallationManagerTest.php | 22 +-- .../Test/Installer/InstallerEventTest.php | 6 +- tests/Composer/Test/Mock/FactoryMock.php | 3 +- .../Test/Mock/InstallationManagerMock.php | 10 +- 12 files changed, 222 insertions(+), 123 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 07b39fd9b..f84e34884 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -356,7 +356,7 @@ EOT ->setPreferDist($preferDist); $projectInstaller = new ProjectInstaller($directory, $dm); - $im = $factory->createInstallationManager(new Loop($httpDownloader)); + $im = $factory->createInstallationManager(new Loop($httpDownloader), $io, $composer->getEventDispatcher); $im->addInstaller($projectInstaller); $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package)); $im->notifyInstalls($io); diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index f37b1a2a4..f4e54611b 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -100,21 +100,18 @@ class EventDispatcher /** * Dispatch a package event. * - * @param string $eventName The constant in PackageEvents - * @param bool $devMode Whether or not we are in dev mode - * @param PolicyInterface $policy The policy - * @param RepositorySet $repositorySet The repository set - * @param CompositeRepository $installedRepo The installed repository - * @param Request $request The request - * @param array $operations The list of operations - * @param OperationInterface $operation The package being installed/updated/removed + * @param string $eventName The constant in PackageEvents + * @param bool $devMode Whether or not we are in dev mode + * @param RepositoryInterface $localRepo The installed repository + * @param array $operations The list of operations + * @param OperationInterface $operation The package being installed/updated/removed * * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchPackageEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation) + public function dispatchPackageEvent($eventName, $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation) { - return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation)); + return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $localRepo, $operations, $operation)); } /** @@ -124,16 +121,16 @@ class EventDispatcher * @param bool $devMode Whether or not we are in dev mode * @param PolicyInterface $policy The policy * @param RepositorySet $repositorySet The repository set - * @param CompositeRepository $installedRepo The installed repository + * @param RepositoryInterface $localRepo The installed repository * @param Request $request The request * @param array $operations The list of operations * * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $lockedRepo, Request $request, array $operations = array()) + public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $localRepo, Request $request, array $operations = array()) { - return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $lockedRepo, $request, $operations)); + return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $localRepo, $request, $operations)); } /** diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index b6dc373a5..02c786119 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -354,7 +354,7 @@ class Factory $composer->setPackage($package); // initialize installation manager - $im = $this->createInstallationManager($loop); + $im = $this->createInstallationManager($loop, $io, $dispatcher); $composer->setInstallationManager($im); if ($fullLoad) { @@ -527,9 +527,9 @@ class Factory /** * @return Installer\InstallationManager */ - public function createInstallationManager(Loop $loop) + public function createInstallationManager(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) { - return new Installer\InstallationManager($loop); + return new Installer\InstallationManager($loop, $io, $eventDispatcher); } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9f014be1e..60cde1a76 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -212,7 +212,6 @@ class Installer $this->executeOperations = false; $this->writeLock = false; $this->dumpAutoloader = false; - $this->installationManager->addInstaller(new NoopInstaller); $this->mockLocalRepositories($this->repositoryManager); } @@ -664,27 +663,14 @@ class Installer } } - foreach ($localRepoTransaction->getOperations() as $operation) { - $jobType = $operation->getJobType(); - $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($jobType); - if (defined($event) && $this->runScripts) { - //$this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); - } - - // output op, but alias op only in debug verbosity - if ((!$this->executeOperations && false === strpos($operation->getJobType(), 'Alias')) || $this->io->isDebug()) { - $this->io->writeError(' - ' . $operation->show(false)); - } - - $this->installationManager->execute($localRepo, $operation); - - if ($this->executeOperations) { - $localRepo->write($this->devMode, $this->installationManager); - } - - $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); - if (defined($event) && $this->runScripts) { - //$this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); + if ($this->executeOperations) { + $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode); + } else { + foreach ($localRepoTransaction->getOperations() as $operation) { + // output op, but alias op only in debug verbosity + if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { + $this->io->writeError(' - ' . $operation->show(false)); + } } } diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index f018c0a31..6bb5b9d4e 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -23,6 +23,7 @@ use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; +use Composer\EventDispatcher\EventDispatcher; use Composer\Util\StreamContextFactory; use Composer\Util\Loop; @@ -39,10 +40,14 @@ class InstallationManager private $cache = array(); private $notifiablePackages = array(); private $loop; + private $io; + private $eventDispatcher; - public function __construct(Loop $loop) + public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) { $this->loop = $loop; + $this->io = $io; + $this->eventDispatcher = $eventDispatcher; } public function reset() @@ -158,70 +163,100 @@ class InstallationManager /** * Executes solver operation. * - * @param RepositoryInterface $repo repository in which to check - * @param OperationInterface $operation operation instance + * @param RepositoryInterface $repo repository in which to add/remove/update packages + * @param OperationInterface[] $operations operations to execute + * @param bool $devMode whether the install is being run in dev mode + * @param bool $operation whether to dispatch script events */ - public function execute(RepositoryInterface $repo, OperationInterface $operation) + public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) { - // TODO this should take all operations in one go - $method = $operation->getJobType(); + $promises = array(); - if ($method === 'install') { - $package = $operation->getPackage(); - $installer = $this->getInstaller($package->getType()); - $promise = $installer->download($package); - } elseif ($method === 'update') { - $target = $operation->getTargetPackage(); - $targetType = $target->getType(); - $installer = $this->getInstaller($targetType); - $promise = $installer->download($target, $operation->getInitialPackage()); - } + foreach ($operations as $operation) { + $method = $operation->getJobType(); + $promise = null; - if (!empty($promise)) { - $this->loop->wait(array($promise)); - } - - $e = null; - try { - if ($method === 'install' || $method === 'uninstall') { + if ($method === 'install') { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); - $promise = $installer->prepare($method, $package); + $promise = $installer->download($package); } elseif ($method === 'update') { $target = $operation->getTargetPackage(); $targetType = $target->getType(); $installer = $this->getInstaller($targetType); - $promise = $installer->prepare('update', $target, $operation->getInitialPackage()); + $promise = $installer->download($target, $operation->getInitialPackage()); + } + + if ($promise) { + $promises[] = $promise; + } + } + + if (!empty($promises)) { + $this->loop->wait($promises); + } + + foreach ($operations as $operation) { + $method = $operation->getJobType(); + $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($method); + if (defined($event) && $runScripts && $this->eventDispatcher) { + $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); + } + + // output alias ops in debug verbosity as they have no output otherwise + if ($this->io->isDebug()) { + $this->io->writeError(' - ' . $operation->show(false)); + } + + $e = null; + try { + if ($method === 'install' || $method === 'uninstall') { + $package = $operation->getPackage(); + $installer = $this->getInstaller($package->getType()); + $promise = $installer->prepare($method, $package); + } elseif ($method === 'update') { + $target = $operation->getTargetPackage(); + $targetType = $target->getType(); + $installer = $this->getInstaller($targetType); + $promise = $installer->prepare('update', $target, $operation->getInitialPackage()); + } + + if (!empty($promise)) { + $this->loop->wait(array($promise)); + } + + $promise = $this->$method($repo, $operation); + if (!empty($promise)) { + $this->loop->wait(array($promise)); + } + } catch (\Exception $e) { + } + + if ($method === 'install' || $method === 'uninstall') { + $package = $operation->getPackage(); + $installer = $this->getInstaller($package->getType()); + $promise = $installer->cleanup($method, $package); + } elseif ($method === 'update') { + $target = $operation->getTargetPackage(); + $targetType = $target->getType(); + $installer = $this->getInstaller($targetType); + $promise = $installer->cleanup('update', $target, $operation->getInitialPackage()); } if (!empty($promise)) { $this->loop->wait(array($promise)); } - $promise = $this->$method($repo, $operation); - if (!empty($promise)) { - $this->loop->wait(array($promise)); + if ($e) { + throw $e; } - } catch (\Exception $e) { - } - if ($method === 'install' || $method === 'uninstall') { - $package = $operation->getPackage(); - $installer = $this->getInstaller($package->getType()); - $promise = $installer->cleanup($method, $package); - } elseif ($method === 'update') { - $target = $operation->getTargetPackage(); - $targetType = $target->getType(); - $installer = $this->getInstaller($targetType); - $promise = $installer->cleanup('update', $target, $operation->getInitialPackage()); - } + $repo->write($devMode, $this); - if (!empty($promise)) { - $this->loop->wait(array($promise)); - } - - if ($e) { - throw $e; + $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($method); + if (defined($event) && $runScripts && $this->eventDispatcher) { + $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); + } } } diff --git a/src/Composer/Installer/InstallerEvent.php b/src/Composer/Installer/InstallerEvent.php index 2d30940a9..7752f6920 100644 --- a/src/Composer/Installer/InstallerEvent.php +++ b/src/Composer/Installer/InstallerEvent.php @@ -18,7 +18,7 @@ use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Request; use Composer\EventDispatcher\Event; use Composer\IO\IOInterface; -use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositorySet; /** @@ -54,9 +54,9 @@ class InstallerEvent extends Event private $repositorySet; /** - * @var CompositeRepository + * @var RepositoryInterface */ - private $installedRepo; + private $localRepo; /** * @var Request @@ -77,11 +77,11 @@ class InstallerEvent extends Event * @param bool $devMode * @param PolicyInterface $policy * @param RepositorySet $repositorySet - * @param CompositeRepository $installedRepo + * @param RepositoryInterface $localRepo * @param Request $request * @param OperationInterface[] $operations */ - public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations = array()) + public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $localRepo, Request $request, array $operations = array()) { parent::__construct($eventName); @@ -90,7 +90,7 @@ class InstallerEvent extends Event $this->devMode = $devMode; $this->policy = $policy; $this->repositorySet = $repositorySet; - $this->installedRepo = $installedRepo; + $this->localRepo = $localRepo; $this->request = $request; $this->operations = $operations; } @@ -136,11 +136,11 @@ class InstallerEvent extends Event } /** - * @return CompositeRepository + * @return RepositoryInterface */ - public function getInstalledRepo() + public function getLocalRepo() { - return $this->installedRepo; + return $this->localRepo; } /** diff --git a/src/Composer/Installer/PackageEvent.php b/src/Composer/Installer/PackageEvent.php index d2f427a92..881820c91 100644 --- a/src/Composer/Installer/PackageEvent.php +++ b/src/Composer/Installer/PackageEvent.php @@ -17,18 +17,44 @@ use Composer\IO\IOInterface; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\PolicyInterface; use Composer\DependencyResolver\Request; -use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositorySet; +use Composer\EventDispatcher\Event; /** * The Package Event. * * @author Jordi Boggiano */ -class PackageEvent extends InstallerEvent +class PackageEvent extends Event { /** - * @var OperationInterface The package instance + * @var Composer + */ + private $composer; + + /** + * @var IOInterface + */ + private $io; + + /** + * @var bool + */ + private $devMode; + + /** + * @var RepositoryInterface + */ + private $localRepo; + + /** + * @var OperationInterface[] + */ + private $operations; + + /** + * @var OperationInterface The operation instance which is being executed */ private $operation; @@ -39,20 +65,63 @@ class PackageEvent extends InstallerEvent * @param Composer $composer * @param IOInterface $io * @param bool $devMode - * @param PolicyInterface $policy - * @param RepositorySet $repositorySet - * @param CompositeRepository $installedRepo + * @param RepositoryInterface $localRepo * @param Request $request * @param OperationInterface[] $operations * @param OperationInterface $operation */ - public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation) + public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, RepositoryInterface $localRepo, array $operations = array(), OperationInterface $operation) { - parent::__construct($eventName, $composer, $io, $devMode, $policy, $repositorySet, $installedRepo, $request, $operations); + parent::__construct($eventName); + $this->composer = $composer; + $this->io = $io; + $this->devMode = $devMode; + $this->localRepo = $localRepo; + $this->operations = $operations; $this->operation = $operation; } + /** + * @return Composer + */ + public function getComposer() + { + return $this->composer; + } + + /** + * @return IOInterface + */ + public function getIO() + { + return $this->io; + } + + /** + * @return bool + */ + public function isDevMode() + { + return $this->devMode; + } + + /** + * @return RepositoryInterface + */ + public function getLocalRepo() + { + return $this->localRepo; + } + + /** + * @return OperationInterface[] + */ + public function getOperations() + { + return $this->operations; + } + /** * Returns the package instance. * diff --git a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test index cca859e9f..af8e9771a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test +++ b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test @@ -35,6 +35,15 @@ Updates updateable packages in dry-run mode ] --RUN-- update --dry-run +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Lock file operations: 3 installs, 0 updates, 0 removals + - Locking a/a (1.0.1) + - Locking a/b (2.0.0) + - Locking a/c (1.0.0) +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 2 updates, 0 removals + - Updating a/a (1.0.0) to a/a (1.0.1) + - Updating a/b (1.0.0) to a/b (2.0.0) --EXPECT-- -Updating a/a (1.0.0) to a/a (1.0.1) -Updating a/b (1.0.0) to a/b (2.0.0) diff --git a/tests/Composer/Test/Installer/InstallationManagerTest.php b/tests/Composer/Test/Installer/InstallationManagerTest.php index 407a5b10f..dfa0c06cb 100644 --- a/tests/Composer/Test/Installer/InstallationManagerTest.php +++ b/tests/Composer/Test/Installer/InstallationManagerTest.php @@ -23,11 +23,13 @@ class InstallationManagerTest extends TestCase { protected $repository; protected $loop; + protected $io; public function setUp() { $this->loop = $this->getMockBuilder('Composer\Util\Loop')->disableOriginalConstructor()->getMock(); $this->repository = $this->getMockBuilder('Composer\Repository\InstalledRepositoryInterface')->getMock(); + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); } public function testAddGetInstaller() @@ -41,7 +43,7 @@ class InstallationManagerTest extends TestCase return $arg === 'vendor'; })); - $manager = new InstallationManager($this->loop); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $this->assertSame($installer, $manager->getInstaller('vendor')); @@ -70,7 +72,7 @@ class InstallationManagerTest extends TestCase return $arg === 'vendor'; })); - $manager = new InstallationManager($this->loop); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $this->assertSame($installer, $manager->getInstaller('vendor')); @@ -83,7 +85,7 @@ class InstallationManagerTest extends TestCase public function testExecute() { $manager = $this->getMockBuilder('Composer\Installer\InstallationManager') - ->setConstructorArgs(array($this->loop)) + ->setConstructorArgs(array($this->loop, $this->io)) ->setMethods(array('install', 'update', 'uninstall')) ->getMock(); @@ -112,15 +114,13 @@ class InstallationManagerTest extends TestCase ->with($this->repository, $updateOperation); $manager->addInstaller(new NoopInstaller()); - $manager->execute($this->repository, $installOperation); - $manager->execute($this->repository, $removeOperation); - $manager->execute($this->repository, $updateOperation); + $manager->execute($this->repository, [$installOperation, $removeOperation, $updateOperation]); } public function testInstall() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager($this->loop); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $package = $this->createPackageMock(); @@ -148,7 +148,7 @@ class InstallationManagerTest extends TestCase public function testUpdateWithEqualTypes() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager($this->loop); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $initial = $this->createPackageMock(); @@ -182,7 +182,7 @@ class InstallationManagerTest extends TestCase { $libInstaller = $this->createInstallerMock(); $bundleInstaller = $this->createInstallerMock(); - $manager = new InstallationManager($this->loop); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($libInstaller); $manager->addInstaller($bundleInstaller); @@ -228,7 +228,7 @@ class InstallationManagerTest extends TestCase public function testUninstall() { $installer = $this->createInstallerMock(); - $manager = new InstallationManager($this->loop); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $package = $this->createPackageMock(); @@ -258,7 +258,7 @@ class InstallationManagerTest extends TestCase $installer = $this->getMockBuilder('Composer\Installer\LibraryInstaller') ->disableOriginalConstructor() ->getMock(); - $manager = new InstallationManager($this->loop); + $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); $package = $this->createPackageMock(); diff --git a/tests/Composer/Test/Installer/InstallerEventTest.php b/tests/Composer/Test/Installer/InstallerEventTest.php index 2847432e3..0ac71bd55 100644 --- a/tests/Composer/Test/Installer/InstallerEventTest.php +++ b/tests/Composer/Test/Installer/InstallerEventTest.php @@ -23,10 +23,10 @@ class InstallerEventTest extends TestCase $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); $repositorySet = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); - $installedRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); + $localRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); $operations = array($this->getMockBuilder('Composer\DependencyResolver\Operation\OperationInterface')->getMock()); - $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, $policy, $repositorySet, $installedRepo, $request, $operations); + $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, $policy, $repositorySet, $localRepo, $request, $operations); $this->assertSame('EVENT_NAME', $event->getName()); $this->assertInstanceOf('Composer\Composer', $event->getComposer()); @@ -34,7 +34,7 @@ class InstallerEventTest extends TestCase $this->assertTrue($event->isDevMode()); $this->assertInstanceOf('Composer\DependencyResolver\PolicyInterface', $event->getPolicy()); $this->assertInstanceOf('Composer\Repository\RepositorySet', $event->getRepositorySet()); - $this->assertInstanceOf('Composer\Repository\CompositeRepository', $event->getInstalledRepo()); + $this->assertInstanceOf('Composer\Repository\RepositoryInterface', $event->getLocalRepo()); $this->assertInstanceOf('Composer\DependencyResolver\Request', $event->getRequest()); $this->assertCount(1, $event->getOperations()); } diff --git a/tests/Composer/Test/Mock/FactoryMock.php b/tests/Composer/Test/Mock/FactoryMock.php index fcb93d2cc..d4dc444a0 100644 --- a/tests/Composer/Test/Mock/FactoryMock.php +++ b/tests/Composer/Test/Mock/FactoryMock.php @@ -18,6 +18,7 @@ use Composer\Factory; use Composer\Repository\RepositoryManager; use Composer\Repository\WritableRepositoryInterface; use Composer\Installer; +use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\Test\TestCase; use Composer\Util\Loop; @@ -40,7 +41,7 @@ class FactoryMock extends Factory { } - public function createInstallationManager(Loop $loop) + public function createInstallationManager(Loop $loop, IOInterface $io, EventDispatcher $dispatcher = null) { return new InstallationManagerMock(); } diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index 21e717224..c189e9173 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -35,11 +35,13 @@ class InstallationManagerMock extends InstallationManager } - public function execute(RepositoryInterface $repo, OperationInterface $operation) + public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) { - $method = $operation->getJobType(); - // skipping download() step here for tests - $this->$method($repo, $operation); + foreach ($operations as $operation) { + $method = $operation->getJobType(); + // skipping download() step here for tests + $this->$method($repo, $operation); + } } public function getInstallPath(PackageInterface $package) From 99eb86c5065f32f7eb7d928eaf9184d1b804ef2b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 14 Nov 2019 15:26:25 +0100 Subject: [PATCH 148/321] Avoid outputting "Loading ... from cache" in non-verbose installs --- src/Composer/Downloader/FileDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 20d21804d..7c7e45354 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -127,7 +127,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface // use from cache if it is present and has a valid checksum or we have no checksum to check against if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) { if ($output) { - $io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache"); + $io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache", true, IOInterface::VERY_VERBOSE); } $result = \React\Promise\resolve($fileName); } else { From 4e7702598e2586955aabb30dfeb87f42456f78fd Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 14 Nov 2019 15:32:38 +0100 Subject: [PATCH 149/321] Always treat changes in dist and source references as updates --- src/Composer/DependencyResolver/Transaction.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index 04e2e966d..b8e4499de 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -141,9 +141,10 @@ class Transaction // do we need to update? // TODO different for lock? - if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion()) { - $operations[] = new Operation\UpdateOperation($source, $package); - } elseif ($package->isDev() && $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference()) { + if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion() || + $package->getDistReference() !== $presentPackageMap[$package->getName()]->getDistReference() || + $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference() + ) { $operations[] = new Operation\UpdateOperation($source, $package); } unset($removeMap[$package->getName()]); From 438c45e991f9009afbd5896dd67a3c9ec19d5bb4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 14 Nov 2019 15:40:59 +0100 Subject: [PATCH 150/321] Change UpdateOperation::show to match what the installers output, fixes the dry-run output to be more like real output --- .../DependencyResolver/Operation/UpdateOperation.php | 3 +-- .../Test/Fixtures/installer/github-issues-4795-2.test | 8 ++++---- .../installer/install-from-lock-removes-package.test | 2 +- ...artial-update-downgrades-non-whitelisted-unstable.test | 6 +++--- ...-dev-reference-from-lock-for-non-updated-packages.test | 4 ++-- .../Test/Fixtures/installer/partial-update-from-lock.test | 6 +++--- .../partial-update-installs-from-lock-even-missing.test | 4 ++-- .../root-requirements-do-not-affect-locked-versions.test | 2 +- .../Test/Fixtures/installer/update-alias-lock.test | 2 +- tests/Composer/Test/Fixtures/installer/update-alias.test | 4 ++-- .../Test/Fixtures/installer/update-all-dry-run.test | 4 ++-- tests/Composer/Test/Fixtures/installer/update-all.test | 4 ++-- .../Test/Fixtures/installer/update-changes-url.test | 4 ++-- .../Fixtures/installer/update-dev-ignores-providers.test | 2 +- .../installer/update-dev-packages-updates-repo-url.test | 2 +- .../installer/update-dev-to-new-ref-picks-up-changes.test | 2 +- .../installer/update-downgrades-unstable-packages.test | 2 +- .../update-ignore-platform-package-requirements.test | 2 +- .../installer/update-no-dev-still-resolves-dev.test | 4 ++-- .../Fixtures/installer/update-prefer-lowest-stable.test | 4 ++-- .../Test/Fixtures/installer/update-reference.test | 2 +- .../installer/update-whitelist-locked-require.test | 4 ++-- .../update-whitelist-patterns-with-all-dependencies.test | 6 +++--- .../update-whitelist-patterns-with-dependencies.test | 6 +++--- .../update-whitelist-patterns-with-root-dependencies.test | 6 +++--- .../update-whitelist-patterns-without-dependencies.test | 4 ++-- .../Fixtures/installer/update-whitelist-patterns.test | 8 ++++---- .../Fixtures/installer/update-whitelist-reads-lock.test | 4 ++-- .../installer/update-whitelist-removes-unused.test | 2 +- .../installer/update-whitelist-with-dependencies.test | 4 ++-- .../Test/Fixtures/installer/update-whitelist.test | 2 +- .../Fixtures/installer/update-with-all-dependencies.test | 8 ++++---- .../updating-dev-from-lock-removes-old-deps.test | 2 +- .../installer/updating-dev-updates-url-and-reference.test | 2 +- 34 files changed, 65 insertions(+), 66 deletions(-) diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index aecde33c2..a1aec4413 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -74,8 +74,7 @@ class UpdateOperation extends SolverOperation */ public function show($lock) { - return 'Updating '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).') to '. - $this->targetPackage->getPrettyName(). ' ('.$this->formatVersion($this->targetPackage).')'; + return 'Updating '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).' => '.$this->formatVersion($this->targetPackage).')'; } /** diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index e7730cefc..454215ee0 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -55,13 +55,13 @@ update a/a b/b --with-dependencies Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 2 updates, 0 removals - - Updating a/a (1.0.0) to a/a (1.1.0) - - Updating b/b (1.0.0) to b/b (1.1.0) + - Updating a/a (1.0.0 => 1.1.0) + - Updating b/b (1.0.0 => 1.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- -Updating a/a (1.0.0) to a/a (1.1.0) -Updating b/b (1.0.0) to b/b (1.1.0) +Updating a/a (1.0.0 => 1.1.0) +Updating b/b (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index 83569bfe7..7e7f0dfb5 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -41,4 +41,4 @@ Install from a lock file that deleted a package install --EXPECT-- Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index add46b848..d87df634c 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -66,7 +66,7 @@ update c/uptodate "platform-dev": [] } --EXPECT-- -Updating a/old (0.9.0) to a/old (1.0.0) -Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) -Updating c/uptodate (2.0.0) to c/uptodate (1.0.0) +Updating a/old (0.9.0 => 1.0.0) +Updating b/unstable (1.1.0-alpha => 1.0.0) +Updating c/uptodate (2.0.0 => 1.0.0) Installing d/removed (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test index 4533d5a94..d3da18878 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test @@ -93,5 +93,5 @@ update b/b "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster-a) to a/a (dev-master newmaster-a) -Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2) +Updating a/a (dev-master oldmaster-a => dev-master newmaster-a) +Updating b/b (dev-master oldmaster-b => dev-master newmaster-b2) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test index f49f23dfd..faffbead0 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -74,8 +74,8 @@ update b/unstable "platform-dev": [] } --EXPECT-- -Updating a/old (0.9.0) to a/old (1.0.0) -Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) -Updating c/uptodate (2.0.0) to c/uptodate (1.0.0) +Updating a/old (0.9.0 => 1.0.0) +Updating b/unstable (1.1.0-alpha => 1.0.0) +Updating c/uptodate (2.0.0 => 1.0.0) Installing d/removed (1.0.0) Installing e/newreq (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test index ea4a7c997..b21905e07 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test @@ -97,9 +97,9 @@ update b/b "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster-a) to a/a (dev-master newmaster-a) +Updating a/a (dev-master oldmaster-a => dev-master newmaster-a) Marking a/a (2.2.x-dev newmaster-a) as installed, alias of a/a (dev-master newmaster-a) -Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2) +Updating b/b (dev-master oldmaster-b => dev-master newmaster-b2) Marking b/b (2.3.x-dev newmaster-b2) as installed, alias of b/b (dev-master newmaster-b2) Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a) Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) diff --git a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test index 8868cbe13..1895992f6 100644 --- a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test +++ b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test @@ -38,4 +38,4 @@ The locked version will not get overwritten by an install --RUN-- install --EXPECT-- -Updating foo/baz (1.0.0) to foo/baz (2.0.0) +Updating foo/baz (1.0.0 => 2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index da4faeed8..1018fc678 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -59,4 +59,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master 1234) to a/a (dev-master master) +Updating a/a (dev-master 1234 => dev-master master) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias.test b/tests/Composer/Test/Fixtures/installer/update-alias.test index c1020e33c..6a7e434c8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias.test @@ -33,5 +33,5 @@ Update aliased package to non-aliased version --RUN-- update --EXPECT-- -Updating a/a (dev-master master) to a/a (dev-foo foo) -Marking a/a (1.0.x-dev master) as uninstalled, alias of a/a (dev-master master) \ No newline at end of file +Updating a/a (dev-master master => dev-foo foo) +Marking a/a (1.0.x-dev master) as uninstalled, alias of a/a (dev-master master) diff --git a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test index af8e9771a..f3484d93b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test +++ b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test @@ -44,6 +44,6 @@ Lock file operations: 3 installs, 0 updates, 0 removals - Locking a/c (1.0.0) Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals - - Updating a/a (1.0.0) to a/a (1.0.1) - - Updating a/b (1.0.0) to a/b (2.0.0) + - Updating a/a (1.0.0 => 1.0.1) + - Updating a/b (1.0.0 => 2.0.0) --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-all.test b/tests/Composer/Test/Fixtures/installer/update-all.test index a9bb435a1..9a5fe8e9d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all.test +++ b/tests/Composer/Test/Fixtures/installer/update-all.test @@ -36,5 +36,5 @@ Updates updateable packages --RUN-- update --EXPECT-- -Updating a/a (1.0.0) to a/a (1.0.1) -Updating a/b (1.0.0) to a/b (2.0.0) +Updating a/a (1.0.0 => 1.0.1) +Updating a/b (1.0.0 => 2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 3df99678c..bfecf37b8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -214,6 +214,6 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an --RUN-- update a/a b/b d/d g/g --EXPECT-- -Updating a/a (dev-master 1111111) to a/a (dev-master 2222222) +Updating a/a (dev-master 1111111 => dev-master 2222222) Installing e/e (dev-master 1111111) -Updating g/g (dev-master 0000000) to g/g (dev-master 1111111) +Updating g/g (dev-master 0000000 => dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test b/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test index e0858e054..a1ae85a88 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test @@ -35,4 +35,4 @@ Updating a dev package selects its newest version but no providers --RUN-- update --EXPECT-- -Updating a/installed (dev-master oldref) to a/installed (dev-master newref) +Updating a/installed (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test b/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test index f91a67cea..7faef167c 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test @@ -93,4 +93,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster) to a/a (dev-master newmaster) +Updating a/a (dev-master oldmaster => dev-master newmaster) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test b/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test index fa2146345..c84385e64 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test @@ -38,4 +38,4 @@ Updating a dev package to its latest ref should pick up new dependencies update --EXPECT-- Installing a/dependency (dev-master ref) -Updating a/devpackage (dev-master oldref) to a/devpackage (dev-master newref) +Updating a/devpackage (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test index 1b6e55ef9..f418cd33d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test +++ b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test @@ -46,4 +46,4 @@ Downgrading from unstable to more stable package should work even if already ins --RUN-- update --EXPECT-- -Updating a/a (dev-master abcd) to a/a (1.0.0) +Updating a/a (dev-master abcd => 1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test index 02f94cd6e..b12dae58b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test +++ b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test @@ -23,4 +23,4 @@ Update in ignore-platform-reqs mode --RUN-- update --ignore-platform-reqs --EXPECT-- -Updating a/a (1.0.0) to a/a (1.0.1) +Updating a/a (1.0.0 => 1.0.1) diff --git a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test index fad73f20a..17d16df6d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test @@ -61,8 +61,8 @@ Updates with --no-dev but we still end up with a complete lock file including de update --no-dev --EXPECT-- Uninstalling a/b (1.0.0) -Updating a/a (1.0.0) to a/a (1.0.1) +Updating a/a (1.0.0 => 1.0.1) Installing a/c (1.0.0) -Updating dev/pkg (dev-master old) to dev/pkg (dev-master new) +Updating dev/pkg (dev-master old => dev-master new) Marking dev/pkg (1.1.x-dev new) as installed, alias of dev/pkg (dev-master new) Marking dev/pkg (1.0.x-dev old) as uninstalled, alias of dev/pkg (dev-master old) diff --git a/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test index 00efd5688..cd3988a9e 100644 --- a/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test +++ b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test @@ -36,5 +36,5 @@ Updates packages to their lowest stable version --RUN-- update --prefer-lowest --prefer-stable --EXPECT-- -Updating a/a (1.0.0-rc1) to a/a (1.0.1) -Updating a/b (1.0.1) to a/b (1.0.0) +Updating a/a (1.0.0-rc1 => 1.0.1) +Updating a/b (1.0.1 => 1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-reference.test b/tests/Composer/Test/Fixtures/installer/update-reference.test index 9dca245ee..01ba528fe 100644 --- a/tests/Composer/Test/Fixtures/installer/update-reference.test +++ b/tests/Composer/Test/Fixtures/installer/update-reference.test @@ -27,4 +27,4 @@ Updates a dev package forcing it's reference --RUN-- install --EXPECT-- -Updating a/a (dev-master abc123) to a/a (dev-master def000) +Updating a/a (dev-master abc123 => dev-master def000) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test index c3b20426e..aaa29cd6e 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test @@ -49,5 +49,5 @@ Update with a package whitelist only updates those packages if they are not pres --RUN-- update whitelisted dependency --EXPECT-- -Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating dependency (1.0.0 => 1.1.0) +Updating whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test index b3396b376..a76937e43 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test @@ -60,6 +60,6 @@ Update with a package whitelist pattern and all-dependencies flag updates packag --RUN-- update whitelisted-* --with-all-dependencies --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) -Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating whitelisted-component1 (1.0.0 => 1.1.0) +Updating dependency (1.0.0 => 1.1.0) +Updating whitelisted-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test index aee2310ac..3cd619d22 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test @@ -62,6 +62,6 @@ Update with a package whitelist only updates those packages and their dependenci --RUN-- update whitelisted-* --with-dependencies --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) -Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating whitelisted-component1 (1.0.0 => 1.1.0) +Updating dependency (1.0.0 => 1.1.0) +Updating whitelisted-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test index fe0cb1e31..c4e879df8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test @@ -72,6 +72,6 @@ Update with a package whitelist only updates those packages and their dependenci --RUN-- update whitelisted-* --with-dependencies --EXPECT-- -Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) +Updating dependency (1.0.0 => 1.1.0) +Updating whitelisted-component2 (1.0.0 => 1.1.0) +Updating whitelisted-component1 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test index ac07f23fd..eafec1403 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test @@ -57,5 +57,5 @@ Update with a package whitelist only updates those packages matching the pattern --RUN-- update whitelisted-* --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating whitelisted-component1 (1.0.0 => 1.1.0) +Updating whitelisted-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test index d1b4fd66d..54da86c8e 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test @@ -63,7 +63,7 @@ Update with a package whitelist only updates those corresponding to the pattern --RUN-- update vendor/Test* exact/Test-Package notexact/Test all/* no/reg?xp --EXPECT-- -Updating all/Package1 (1.0) to all/Package1 (2.0) -Updating all/Package2 (1.0) to all/Package2 (2.0) -Updating exact/Test-Package (1.0) to exact/Test-Package (2.0) -Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0) +Updating all/Package1 (1.0 => 2.0) +Updating all/Package2 (1.0 => 2.0) +Updating exact/Test-Package (1.0 => 2.0) +Updating vendor/Test-Package (1.0 => 2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test index 552051565..1c2a68a10 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test @@ -43,6 +43,6 @@ Limited update takes rules from lock if available, and not from the installed re --RUN-- update toupdate/installed --EXPECT-- -Updating old/installed (0.9.0) to old/installed (1.0.0) -Updating toupdate/installed (1.0.0) to toupdate/installed (1.1.0) +Updating old/installed (0.9.0 => 1.0.0) +Updating toupdate/installed (1.0.0 => 1.1.0) Installing toupdate/notinstalled (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test index 3b28e4671..93829c808 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -45,4 +45,4 @@ Update with a package whitelist removes unused packages update --with-dependencies whitelisted --EXPECT-- Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test index 73acfa516..2db3b6ead 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test @@ -52,5 +52,5 @@ Update with a package whitelist only updates those packages and their dependenci --RUN-- update whitelisted --with-dependencies --EXPECT-- -Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating dependency (1.0.0 => 1.1.0) +Updating whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index b53954dc6..4dfb95a93 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -54,4 +54,4 @@ Update with a package whitelist only updates those packages listed as command ar --RUN-- update whitelisted --EXPECT-- -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index 8a72cec66..f761abe38 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -50,13 +50,13 @@ update b/b --with-all-dependencies Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 2 updates, 0 removals - - Updating a/a (1.0.0) to a/a (1.1.0) - - Updating b/b (1.0.0) to b/b (1.1.0) + - Updating a/a (1.0.0 => 1.1.0) + - Updating b/b (1.0.0 => 1.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- -Updating a/a (1.0.0) to a/a (1.1.0) -Updating b/b (1.0.0) to b/b (1.1.0) +Updating a/a (1.0.0 => 1.1.0) +Updating b/b (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index 6575530d0..e9f6d825a 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -42,4 +42,4 @@ Installing locked dev packages should remove old dependencies install --EXPECT-- Uninstalling a/dependency (dev-master ref) -Updating a/devpackage (dev-master oldref) to a/devpackage (dev-master newref) +Updating a/devpackage (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test index 06870c4ee..f3aa1816e 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -65,4 +65,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldref) to a/a (dev-master newref) +Updating a/a (dev-master oldref => dev-master newref) From 5725a2db57eb49af03cce6374b794f69a930c359 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 23 Nov 2019 11:25:28 +0100 Subject: [PATCH 151/321] Allow all prepare/cleanup/install/update/uninstall operations to return promises to be executed in parallel as well --- .../Installer/InstallationManager.php | 124 ++++++++++-------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 6bb5b9d4e..c767b5a14 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -173,14 +173,14 @@ class InstallationManager $promises = array(); foreach ($operations as $operation) { - $method = $operation->getJobType(); + $jobType = $operation->getJobType(); $promise = null; - if ($method === 'install') { + if ($jobType === 'install') { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); $promise = $installer->download($package); - } elseif ($method === 'update') { + } elseif ($jobType === 'update') { $target = $operation->getTargetPackage(); $targetType = $target->getType(); $installer = $this->getInstaller($targetType); @@ -197,66 +197,80 @@ class InstallationManager } foreach ($operations as $operation) { - $method = $operation->getJobType(); - $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($method); - if (defined($event) && $runScripts && $this->eventDispatcher) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); + $jobType = $operation->getJobType(); + + // ignoring alias ops as they don't need to execute anything + if (!in_array($jobType, array('update', 'install', 'uninstall'))) { + // output alias ops in debug verbosity as they have no output otherwise + if ($this->io->isDebug()) { + $this->io->writeError(' - ' . $operation->show(false)); + } + continue; } - // output alias ops in debug verbosity as they have no output otherwise - if ($this->io->isDebug()) { - $this->io->writeError(' - ' . $operation->show(false)); - } - - $e = null; - try { - if ($method === 'install' || $method === 'uninstall') { - $package = $operation->getPackage(); - $installer = $this->getInstaller($package->getType()); - $promise = $installer->prepare($method, $package); - } elseif ($method === 'update') { - $target = $operation->getTargetPackage(); - $targetType = $target->getType(); - $installer = $this->getInstaller($targetType); - $promise = $installer->prepare('update', $target, $operation->getInitialPackage()); - } - - if (!empty($promise)) { - $this->loop->wait(array($promise)); - } - - $promise = $this->$method($repo, $operation); - if (!empty($promise)) { - $this->loop->wait(array($promise)); - } - } catch (\Exception $e) { - } - - if ($method === 'install' || $method === 'uninstall') { + if ($jobType === 'install' || $jobType === 'uninstall') { $package = $operation->getPackage(); - $installer = $this->getInstaller($package->getType()); - $promise = $installer->cleanup($method, $package); - } elseif ($method === 'update') { - $target = $operation->getTargetPackage(); - $targetType = $target->getType(); - $installer = $this->getInstaller($targetType); - $promise = $installer->cleanup('update', $target, $operation->getInitialPackage()); + $initialPackage = null; + } elseif ($jobType === 'update') { + $package = $operation->getTargetPackage(); + $initialPackage = $operation->getInitialPackage(); } + $installer = $this->getInstaller($package->getType()); - if (!empty($promise)) { - $this->loop->wait(array($promise)); - } - - if ($e) { - throw $e; - } - - $repo->write($devMode, $this); - - $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($method); + $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($jobType); if (defined($event) && $runScripts && $this->eventDispatcher) { $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); } + + $dispatcher = $this->eventDispatcher; + $installManager = $this; + $loop = $this->loop; + $io = $this->io; + + $promise = new \React\Promise\Promise(function ($resolve, $reject) use ($installer, $jobType, $package, $initialPackage) { + $promise = $installer->prepare($jobType, $package, $initialPackage); + if (null === $promise) { + $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); + } + + return $promise->then($resolve, $reject); + }); + + $promise = $promise->then(function () use ($jobType, $installManager, $repo, $operation) { + $promise = $installManager->$jobType($repo, $operation); + if (null === $promise) { + $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); + } + + return $promise; + })->then(function () use ($jobType, $installer, $package, $initialPackage) { + $promise = $installer->cleanup($jobType, $package, $initialPackage); + if (null === $promise) { + $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); + } + + return $promise; + })->then(function () use ($jobType, $installer, $package, $initialPackage, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { + $repo->write($devMode, $installManager); + + $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); + if (defined($event) && $runScripts && $dispatcher) { + $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); + } + }, function ($e) use ($jobType, $installer, $package, $initialPackage, $loop, $io) { + $this->io->writeError(' ' . ucfirst($jobType) .' of '.$package->getPrettyName().' failed'); + + $promise = $installer->cleanup($jobType, $package, $initialPackage); + $loop->wait(array($promise)); + + throw $e; + }); + + $promises[] = $promise; + } + + if (!empty($promises)) { + $this->loop->wait($promises); } } From f288acb1caffef4e41209b8208ac5797efaba99e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 23 Nov 2019 11:29:50 +0100 Subject: [PATCH 152/321] Execute alias operations still --- src/Composer/Installer/InstallationManager.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index c767b5a14..0ee5b01f6 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -205,6 +205,8 @@ class InstallationManager if ($this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(false)); } + $this->$jobType($repo, $operation); + continue; } From a732ea5dd8918e82b6b08c4df3c870e714410a71 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 23 Nov 2019 12:31:33 +0100 Subject: [PATCH 153/321] Display source/dist refs if updates do not change the version We now output an update when only a source or dist ref changes even if it's a stable version so in these cases the output needs to contain the ref to make it clear to users what changed --- .../Operation/InstallOperation.php | 2 +- .../Operation/MarkAliasInstalledOperation.php | 2 +- .../MarkAliasUninstalledOperation.php | 2 +- .../Operation/SolverOperation.php | 5 ---- .../Operation/UninstallOperation.php | 2 +- .../Operation/UpdateOperation.php | 13 +++++++++- src/Composer/Package/BasePackage.php | 24 ++++++++++++++----- src/Composer/Package/PackageInterface.php | 7 +++++- .../installer/update-changes-url.test | 1 + .../installer/update-installed-reference.test | 1 + .../update-picks-up-change-of-vcs-type.test | 2 +- 11 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/Composer/DependencyResolver/Operation/InstallOperation.php b/src/Composer/DependencyResolver/Operation/InstallOperation.php index 569ae67a4..c451446b5 100644 --- a/src/Composer/DependencyResolver/Operation/InstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/InstallOperation.php @@ -61,7 +61,7 @@ class InstallOperation extends SolverOperation */ public function show($lock) { - return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; + return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; } /** diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php index 800cf43c2..7cecf565c 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php @@ -62,7 +62,7 @@ class MarkAliasInstalledOperation extends SolverOperation */ public function show($lock) { - return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')'; + return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')'; } /** diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php index 3c996f343..c84acf5af 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php @@ -62,7 +62,7 @@ class MarkAliasUninstalledOperation extends SolverOperation */ public function show($lock) { - return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')'; + return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')'; } /** diff --git a/src/Composer/DependencyResolver/Operation/SolverOperation.php b/src/Composer/DependencyResolver/Operation/SolverOperation.php index bb733b70f..bbc077c31 100644 --- a/src/Composer/DependencyResolver/Operation/SolverOperation.php +++ b/src/Composer/DependencyResolver/Operation/SolverOperation.php @@ -48,9 +48,4 @@ abstract class SolverOperation implements OperationInterface * @return string */ abstract public function show($lock); - - protected function formatVersion(PackageInterface $package) - { - return $package->getFullPrettyVersion(); - } } diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index 73321f96f..0f990e57b 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -61,7 +61,7 @@ class UninstallOperation extends SolverOperation */ public function show($lock) { - return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; + return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; } /** diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index a1aec4413..d846f3da1 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -74,7 +74,18 @@ class UpdateOperation extends SolverOperation */ public function show($lock) { - return 'Updating '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).' => '.$this->formatVersion($this->targetPackage).')'; + $fromVersion = $this->initialPackage->getFullPrettyVersion(); + $toVersion = $this->targetPackage->getFullPrettyVersion(); + + if ($fromVersion === $toVersion && $this->initialPackage->getSourceReference() !== $this->targetPackage->getSourceReference()) { + $fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); + $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); + } elseif ($fromVersion === $toVersion && $this->initialPackage->getDistReference() !== $this->targetPackage->getDistReference()) { + $fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); + $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); + } + + return 'Updating '.$this->initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')'; } /** diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 9630e7ef0..0c40a3016 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -210,18 +210,30 @@ abstract class BasePackage implements PackageInterface /** * {@inheritDoc} */ - public function getFullPrettyVersion($truncate = true) + public function getFullPrettyVersion($truncate = true, $displayMode = PackageInterface::DISPLAY_SOURCE_REF_IF_DEV) { - if (!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git'))) { + if ($displayMode === PackageInterface::DISPLAY_SOURCE_REF_IF_DEV && + (!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git'))) + ) { return $this->getPrettyVersion(); } - // if source reference is a sha1 hash -- truncate - if ($truncate && strlen($this->getSourceReference()) === 40) { - return $this->getPrettyVersion() . ' ' . substr($this->getSourceReference(), 0, 7); + switch ($displayMode) { + case PackageInterface::DISPLAY_SOURCE_REF_IF_DEV: + case PackageInterface::DISPLAY_SOURCE_REF: + $reference = $this->getSourceReference(); + break; + case PackageInterface::DISPLAY_DIST_REF: + $reference = $this->getDistReference(); + break; } - return $this->getPrettyVersion() . ' ' . $this->getSourceReference(); + // if source reference is a sha1 hash -- truncate + if ($truncate && strlen($reference) === 40) { + return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7); + } + + return $this->getPrettyVersion() . ' ' . $reference; } public function getStabilityPriority() diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 7e83839ff..9db188358 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -21,6 +21,10 @@ use Composer\Repository\RepositoryInterface; */ interface PackageInterface { + const DISPLAY_SOURCE_REF_IF_DEV = 0; + const DISPLAY_SOURCE_REF = 1; + const DISPLAY_DIST_REF = 2; + /** * Returns the package's name without version info, thus not a unique identifier * @@ -198,9 +202,10 @@ interface PackageInterface * @see getPrettyVersion * * @param bool $truncate If the source reference is a sha1 hash, truncate it + * @param int $displayMode One of the DISPLAY_ constants on this interface determining display of references * @return string version */ - public function getFullPrettyVersion($truncate = true); + public function getFullPrettyVersion($truncate = true, $displayMode = self::DISPLAY_SOURCE_REF_IF_DEV); /** * Returns the release date of the package diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index bfecf37b8..e9c703f41 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -215,5 +215,6 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an update a/a b/b d/d g/g --EXPECT-- Updating a/a (dev-master 1111111 => dev-master 2222222) +Updating b/b (2.0.3 1111111 => 2.0.3 2222222) Installing e/e (dev-master 1111111) Updating g/g (dev-master 0000000 => dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-installed-reference.test b/tests/Composer/Test/Fixtures/installer/update-installed-reference.test index e6814ccfe..7c8df0459 100644 --- a/tests/Composer/Test/Fixtures/installer/update-installed-reference.test +++ b/tests/Composer/Test/Fixtures/installer/update-installed-reference.test @@ -28,3 +28,4 @@ Updating a dev package forcing it's reference should not do anything if the refe --RUN-- update --EXPECT-- +Updating a/a (dev-master def000 => dev-master ) diff --git a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test index a82487a31..18213d3b9 100644 --- a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test +++ b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test @@ -62,4 +62,4 @@ update mirrors "platform-dev": [] } --EXPECT-- - +Updating a/a (1.0.0 old-hg-ref => 1.0.0 new-git-ref) From 5c62c7d566ac3a883a1f573a43b59f7f040edd9d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 23 Nov 2019 12:37:01 +0100 Subject: [PATCH 154/321] Simplify promise handling --- .../Installer/InstallationManager.php | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 0ee5b01f6..7cf3c7128 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -229,29 +229,15 @@ class InstallationManager $loop = $this->loop; $io = $this->io; - $promise = new \React\Promise\Promise(function ($resolve, $reject) use ($installer, $jobType, $package, $initialPackage) { - $promise = $installer->prepare($jobType, $package, $initialPackage); - if (null === $promise) { - $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); - } - - return $promise->then($resolve, $reject); - }); + $promise = $installer->prepare($jobType, $package, $initialPackage); + if (null === $promise) { + $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); + } $promise = $promise->then(function () use ($jobType, $installManager, $repo, $operation) { - $promise = $installManager->$jobType($repo, $operation); - if (null === $promise) { - $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); - } - - return $promise; + return $installManager->$jobType($repo, $operation); })->then(function () use ($jobType, $installer, $package, $initialPackage) { - $promise = $installer->cleanup($jobType, $package, $initialPackage); - if (null === $promise) { - $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); - } - - return $promise; + return $installer->cleanup($jobType, $package, $initialPackage); })->then(function () use ($jobType, $installer, $package, $initialPackage, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { $repo->write($devMode, $installManager); @@ -263,7 +249,9 @@ class InstallationManager $this->io->writeError(' ' . ucfirst($jobType) .' of '.$package->getPrettyName().' failed'); $promise = $installer->cleanup($jobType, $package, $initialPackage); - $loop->wait(array($promise)); + if ($promise) { + $loop->wait(array($promise)); + } throw $e; }); From 29612e8e8e8a8a2e3ac801198d4034668db37cc4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 23 Nov 2019 13:11:19 +0100 Subject: [PATCH 155/321] Remove dead code --- src/Composer/Command/CreateProjectCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index f84e34884..19c7b4a72 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -356,7 +356,7 @@ EOT ->setPreferDist($preferDist); $projectInstaller = new ProjectInstaller($directory, $dm); - $im = $factory->createInstallationManager(new Loop($httpDownloader), $io, $composer->getEventDispatcher); + $im = $factory->createInstallationManager(new Loop($httpDownloader), $io); $im->addInstaller($projectInstaller); $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package)); $im->notifyInstalls($io); From 3b0339802bae2c7afc65882a49d7eff92596d837 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 23 Nov 2019 13:12:13 +0100 Subject: [PATCH 156/321] Fix install manager usage --- src/Composer/Command/CreateProjectCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 19c7b4a72..bb31d97b0 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -358,7 +358,7 @@ EOT $projectInstaller = new ProjectInstaller($directory, $dm); $im = $factory->createInstallationManager(new Loop($httpDownloader), $io); $im->addInstaller($projectInstaller); - $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package)); + $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package))); $im->notifyInstalls($io); // collect suggestions From 88eb61c1d8031e11987fd6f2821c9ec70dc2b527 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 23 Nov 2019 13:18:09 +0100 Subject: [PATCH 157/321] Fix 5.3 syntax --- tests/Composer/Test/Installer/InstallationManagerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Installer/InstallationManagerTest.php b/tests/Composer/Test/Installer/InstallationManagerTest.php index dfa0c06cb..9e12ec215 100644 --- a/tests/Composer/Test/Installer/InstallationManagerTest.php +++ b/tests/Composer/Test/Installer/InstallationManagerTest.php @@ -114,7 +114,7 @@ class InstallationManagerTest extends TestCase ->with($this->repository, $updateOperation); $manager->addInstaller(new NoopInstaller()); - $manager->execute($this->repository, [$installOperation, $removeOperation, $updateOperation]); + $manager->execute($this->repository, array($installOperation, $removeOperation, $updateOperation)); } public function testInstall() From f2058680a7f1fc7cd6b4ef344dd3dff84a3c8933 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 24 Nov 2019 08:46:23 +0100 Subject: [PATCH 158/321] Remove unused use statements --- src/Composer/Installer/InstallationManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 7cf3c7128..9c3fb6884 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -238,7 +238,7 @@ class InstallationManager return $installManager->$jobType($repo, $operation); })->then(function () use ($jobType, $installer, $package, $initialPackage) { return $installer->cleanup($jobType, $package, $initialPackage); - })->then(function () use ($jobType, $installer, $package, $initialPackage, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { + })->then(function () use ($jobType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { $repo->write($devMode, $installManager); $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); From d12c20db4b57663a43db22875721374e62fb00c7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 24 Nov 2019 09:41:02 +0100 Subject: [PATCH 159/321] Remove async repo interface, closes #7902 --- .../DependencyResolver/PoolBuilder.php | 17 +-------- .../Repository/AsyncRepositoryInterface.php | 38 ------------------- 2 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 src/Composer/Repository/AsyncRepositoryInterface.php diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 1c995bc4e..b2f596bbd 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -16,7 +16,6 @@ use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Package; use Composer\Package\PackageInterface; -use Composer\Repository\AsyncRepositoryInterface; use Composer\Repository\ComposerRepository; use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\PlatformRepository; @@ -87,13 +86,6 @@ class PoolBuilder } while (!empty($loadNames)) { - $loadIds = array(); - foreach ($repositories as $key => $repository) { - if ($repository instanceof AsyncRepositoryInterface) { - $loadIds[$key] = $repository->requestPackages($loadNames); - } - } - foreach ($loadNames as $name => $void) { $this->loadedNames[$name] = true; } @@ -104,13 +96,8 @@ class PoolBuilder continue; } - if ($repository instanceof AsyncRepositoryInterface) { - // TODO ispackageacceptablecallable in here? - $packages = $repository->returnPackages($loadIds[$key]); - } else { - // TODO should we really pass the callable into here? - $packages = $repository->loadPackages($loadNames, $this->isPackageAcceptableCallable); - } + // TODO should we really pass the callable into here? + $packages = $repository->loadPackages($loadNames, $this->isPackageAcceptableCallable); foreach ($packages as $package) { if (call_user_func($this->isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { diff --git a/src/Composer/Repository/AsyncRepositoryInterface.php b/src/Composer/Repository/AsyncRepositoryInterface.php deleted file mode 100644 index 5804694ec..000000000 --- a/src/Composer/Repository/AsyncRepositoryInterface.php +++ /dev/null @@ -1,38 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Repository; - -use Composer\Package\PackageInterface; - -/** - * Repository interface. - * - * @author Nils Adermann - * @author Konstantin Kudryashov - * @author Jordi Boggiano - */ -interface AsyncRepositoryInterface -{ - /** - * @param array $names Names of packages to retrieve data for - * @return mixed Id to be passed to later loadPackages call - */ - public function requestPackages(array $names); - - /** - * @param array $names - * @return mixed id for load call - */ - public function returnPackages($loadId); -} - From d1dc367d865cf29af277818b8094901763fd38cb Mon Sep 17 00:00:00 2001 From: Yanick Witschi Date: Sat, 23 Nov 2019 14:43:22 +0100 Subject: [PATCH 160/321] Removed the filters from the pool --- src/Composer/DependencyResolver/Pool.php | 42 +++++-------------- .../DependencyResolver/PoolBuilder.php | 8 ++-- src/Composer/DependencyResolver/Problem.php | 10 +++-- src/Composer/DependencyResolver/Rule.php | 5 ++- src/Composer/Installer.php | 8 ++-- src/Composer/Repository/RepositorySet.php | 12 +++--- .../Fixtures/installer/solver-problems.test | 2 +- 7 files changed, 35 insertions(+), 52 deletions(-) diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index e62d03999..9470c6e35 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -32,20 +32,16 @@ class Pool implements \Countable const MATCH = 1; const MATCH_PROVIDE = 2; const MATCH_REPLACE = 3; - const MATCH_FILTERED = 4; - protected $providerRepos = array(); protected $packages = array(); protected $packageByName = array(); protected $packageByExactName = array(); protected $priorities = array(); protected $versionParser; protected $providerCache = array(); - protected $filterRequires; - public function __construct(array $filterRequires = array()) + public function __construct() { - $this->filterRequires = $filterRequires; $this->versionParser = new VersionParser; } @@ -99,27 +95,22 @@ class Pool implements \Countable * packages must match or null to return all * @param bool $mustMatchName Whether the name of returned packages * must match the given name - * @param bool $bypassFilters If enabled, filterRequires and stability matching is ignored * @return PackageInterface[] A set of packages */ - public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false, $bypassFilters = false) + public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false) { - if ($bypassFilters) { - return $this->computeWhatProvides($name, $constraint, $mustMatchName, true); - } - $key = ((int) $mustMatchName).$constraint; if (isset($this->providerCache[$name][$key])) { return $this->providerCache[$name][$key]; } - return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName, $bypassFilters); + return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName); } /** * @see whatProvides */ - private function computeWhatProvides($name, $constraint, $mustMatchName = false, $bypassFilters = false) + private function computeWhatProvides($name, $constraint, $mustMatchName = false) { $candidates = array(); @@ -135,7 +126,7 @@ class Pool implements \Countable $nameMatch = false; foreach ($candidates as $candidate) { - switch ($this->match($candidate, $name, $constraint, $bypassFilters)) { + switch ($this->match($candidate, $name, $constraint)) { case self::MATCH_NONE: break; @@ -156,9 +147,6 @@ class Pool implements \Countable $matches[] = $candidate; break; - case self::MATCH_FILTERED: - break; - default: throw new \UnexpectedValueException('Unexpected match type'); } @@ -201,24 +189,16 @@ class Pool implements \Countable * @param ConstraintInterface $constraint The constraint to verify * @return int One of the MATCH* constants of this class or 0 if there is no match */ - public function match($candidate, $name, ConstraintInterface $constraint = null, $bypassFilters) + public function match($candidate, $name, ConstraintInterface $constraint = null) { $candidateName = $candidate->getName(); $candidateVersion = $candidate->getVersion(); - $isDev = $candidate->getStability() === 'dev'; - $isAlias = $candidate instanceof AliasPackage; - - if (!$bypassFilters && !$isDev && !$isAlias && isset($this->filterRequires[$name])) { - $requireFilter = $this->filterRequires[$name]; - } else { - $requireFilter = new EmptyConstraint; - } if ($candidateName === $name) { $pkgConstraint = new Constraint('==', $candidateVersion); if ($constraint === null || $constraint->matches($pkgConstraint)) { - return $requireFilter->matches($pkgConstraint) ? self::MATCH : self::MATCH_FILTERED; + return self::MATCH; } return self::MATCH_NAME; @@ -231,13 +211,13 @@ class Pool implements \Countable if (isset($replaces[0]) || isset($provides[0])) { foreach ($provides as $link) { if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { - return $requireFilter->matches($link->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED; + return self::MATCH_PROVIDE; } } foreach ($replaces as $link) { if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { - return $requireFilter->matches($link->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED; + return self::MATCH_REPLACE; } } @@ -245,11 +225,11 @@ class Pool implements \Countable } if (isset($provides[$name]) && ($constraint === null || $constraint->matches($provides[$name]->getConstraint()))) { - return $requireFilter->matches($provides[$name]->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED; + return self::MATCH_PROVIDE; } if (isset($replaces[$name]) && ($constraint === null || $constraint->matches($replaces[$name]->getConstraint()))) { - return $requireFilter->matches($replaces[$name]->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED; + return self::MATCH_REPLACE; } return self::MATCH_NONE; diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index b2f596bbd..7b80c41bd 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -28,7 +28,7 @@ use Composer\Semver\Constraint\MultiConstraint; class PoolBuilder { private $isPackageAcceptableCallable; - private $filterRequires; + private $rootRequires; private $rootAliases; private $rootReferences; @@ -40,15 +40,15 @@ class PoolBuilder private $packages = array(); private $priorities = array(); - public function __construct($isPackageAcceptableCallable, array $filterRequires = array()) + public function __construct($isPackageAcceptableCallable, array $rootRequires = array()) { $this->isPackageAcceptableCallable = $isPackageAcceptableCallable; - $this->filterRequires = $filterRequires; + $this->rootRequires = $rootRequires; } public function buildPool(array $repositories, array $rootAliases, array $rootReferences, Request $request) { - $pool = new Pool($this->filterRequires); + $pool = new Pool(); $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index c2c3f42c6..8c73bd91d 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -148,13 +148,15 @@ class Problem return "\n - The requested package ".$packageName.' could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'; } - if ($providers = $this->pool->whatProvides($packageName, $constraint, true, true)) { + // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet + /*if ($providers = $this->pool->whatProvides($packageName, $constraint, true, true)) { return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' is satisfiable by '.$this->getPackageList($providers).' but these conflict with your requirements or minimum-stability.'; - } + }*/ - if ($providers = $this->pool->whatProvides($packageName, null, true, true)) { + // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet + /*if ($providers = $this->pool->whatProvides($packageName, null, true, true)) { return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' exists as '.$this->getPackageList($providers).' but these are rejected by your constraint.'; - } + }*/ return "\n - The requested package ".$packageName.' could not be found in any version, there may be a typo in the package name.'; } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index c238f59fa..7a12664db 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -217,9 +217,10 @@ abstract class Rule return $text . ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.'; } - if ($providers = $pool->whatProvides($targetName, $this->reasonData->getConstraint(), true, true)) { + // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet + /*if ($providers = $pool->whatProvides($targetName, $this->reasonData->getConstraint(), true, true)) { return $text . ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $providers) .' but these conflict with your requirements or minimum-stability.'; - } + }*/ return $text . ' -> no matching package found.'; } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 60cde1a76..87412f7ea 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -717,16 +717,16 @@ class Installer } } - $rootConstraints = array(); + $rootRequires = array(); foreach ($requires as $req => $constraint) { // skip platform requirements from the root package to avoid filtering out existing platform packages if ($this->ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req)) { continue; } if ($constraint instanceof Link) { - $rootConstraints[$req] = $constraint->getConstraint(); + $rootRequires[$req] = $constraint->getConstraint(); } else { - $rootConstraints[$req] = $constraint; + $rootRequires[$req] = $constraint; } } @@ -734,7 +734,7 @@ class Installer $this->fixedRootPackage->setRequires(array()); $this->fixedRootPackage->setDevRequires(array()); - $repositorySet = new RepositorySet($rootAliases, $this->package->getReferences(), $minimumStability, $stabilityFlags, $rootConstraints); + $repositorySet = new RepositorySet($rootAliases, $this->package->getReferences(), $minimumStability, $stabilityFlags, $rootRequires); $repositorySet->addRepository(new InstalledArrayRepository(array($this->fixedRootPackage))); $repositorySet->addRepository($platformRepo); if ($this->additionalFixedRepository) { diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index f7f8a57f8..21526b96f 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -37,12 +37,12 @@ class RepositorySet private $acceptableStabilities; private $stabilityFlags; - protected $filterRequires; + protected $rootRequires; /** @var Pool */ private $pool; - public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) + public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $rootRequires = array()) { $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; @@ -54,10 +54,10 @@ class RepositorySet } } $this->stabilityFlags = $stabilityFlags; - $this->filterRequires = $filterRequires; - foreach ($filterRequires as $name => $constraint) { + $this->rootRequires = $rootRequires; + foreach ($rootRequires as $name => $constraint) { if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) { - unset($this->filterRequires[$name]); + unset($this->rootRequires[$name]); } } } @@ -152,7 +152,7 @@ class RepositorySet */ public function createPool(Request $request) { - $poolBuilder = new PoolBuilder(array($this, 'isPackageAcceptable'), $this->filterRequires); + $poolBuilder = new PoolBuilder(array($this, 'isPackageAcceptable'), $this->rootRequires); return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $this->rootReferences, $request); } diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 94a0e3aa3..57d3ced92 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -65,7 +65,7 @@ Your requirements could not be resolved to an installable set of packages. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. Problem 4 - stable-requiree-excluded/pkg is locked to version 1.0.0 and an update of this package was not requested. - - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.1, 1.0.0]. + - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.0, 1.0.1]. - Installation request for stable-requiree-excluded/pkg 1.0.1 -> satisfiable by stable-requiree-excluded/pkg[1.0.1]. Potential causes: From 02b6dc876fc961866528f60cf8696ec472ce94fe Mon Sep 17 00:00:00 2001 From: Yanick Witschi Date: Wed, 27 Nov 2019 17:55:02 +0100 Subject: [PATCH 161/321] Fixed phpstan issues --- tests/Composer/Test/DependencyResolver/PoolTest.php | 2 +- tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php | 2 +- tests/Composer/Test/DependencyResolver/RuleSetTest.php | 2 +- tests/Composer/Test/DependencyResolver/RuleTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Composer/Test/DependencyResolver/PoolTest.php b/tests/Composer/Test/DependencyResolver/PoolTest.php index 5ff92f7b0..dba110691 100644 --- a/tests/Composer/Test/DependencyResolver/PoolTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolTest.php @@ -65,6 +65,6 @@ class PoolTest extends TestCase protected function createPool() { - return new Pool(array('stable' => BasePackage::STABILITY_STABLE)); + return new Pool(); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index 24a2e7c54..783734065 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -27,7 +27,7 @@ class RuleSetIteratorTest extends TestCase protected function setUp() { - $this->pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); + $this->pool = new Pool(); $this->rules = array( RuleSet::TYPE_JOB => array( diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 70f030333..05780e526 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -139,7 +139,7 @@ class RuleSetTest extends TestCase public function testPrettyString() { - $pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); + $pool = new Pool(); $pool->setPackages(array( $p = $this->getPackage('foo', '2.1'), )); diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index ab931a73a..3a0e2e5ca 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -93,7 +93,7 @@ class RuleTest extends TestCase public function testPrettyString() { - $pool = new Pool(array('stable' => BasePackage::STABILITY_STABLE)); + $pool = new Pool(); $pool->setPackages(array( $p1 = $this->getPackage('foo', '2.1'), $p2 = $this->getPackage('baz', '1.1'), From 5fc31716dd4676318b9857f8ce3bd2e9afd6a5ca Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 7 Dec 2019 18:58:17 +0100 Subject: [PATCH 162/321] Avoid using CURLPIPE_HTTP1 in php7.4+ --- src/Composer/Util/Http/CurlDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 99ee1ca3c..ee93ca364 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -74,7 +74,7 @@ class CurlDownloader $this->multiHandle = $mh = curl_multi_init(); if (function_exists('curl_multi_setopt')) { - curl_multi_setopt($mh, CURLMOPT_PIPELINING, /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3); + curl_multi_setopt($mh, CURLMOPT_PIPELINING, PHP_VERSION_ID >= 70400 ? /* CURLPIPE_MULTIPLEX */ 2 : /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3); if (defined('CURLMOPT_MAX_HOST_CONNECTIONS')) { curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 8); } From 48023e95111fb1b2456406969fa8ebeaeda7078d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=B6ller?= Date: Tue, 10 Dec 2019 16:56:04 +0100 Subject: [PATCH 163/321] Enhancement: Configure paths and autoload files via phpstan/config.neon --- .travis.yml | 2 +- phpstan/config.neon | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3b1e2c4f4..0cc13f91a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,7 +65,7 @@ script: # Run PHPStan - if [[ $PHPSTAN == "1" ]]; then bin/composer require --dev phpstan/phpstan-shim:^0.11 --ignore-platform-reqs && - vendor/bin/phpstan.phar analyse src tests --configuration=phpstan/config.neon --autoload-file=phpstan/autoload.php; + vendor/bin/phpstan.phar analyse --configuration=phpstan/config.neon; fi before_deploy: diff --git a/phpstan/config.neon b/phpstan/config.neon index 0c5dc30b8..ae53ac531 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -1,4 +1,6 @@ parameters: + autoload_files: + - phpstan/autoload.php level: 0 excludes_analyse: - 'tests/Composer/Test/Fixtures' @@ -39,3 +41,6 @@ parameters: # parent call in test mocks - '~^Composer\\Test\\Mock\\HttpDownloaderMock::__construct\(\) does not call parent constructor from Composer\\Util\\HttpDownloader\.$~' - '~^Composer\\Test\\Mock\\InstallationManagerMock::__construct\(\) does not call parent constructor from Composer\\Installer\\InstallationManager\.$~' + paths: + - src + - tests From 620497a2ed8d5421de08b98629a329d836051e1e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 Dec 2019 17:21:21 +0100 Subject: [PATCH 164/321] Fix usage of DefaultPolicy in show command --- src/Composer/Command/ShowCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index ffc0776dd..998ea3802 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -563,7 +563,7 @@ EOT $pool = $repositorySet->createPoolForPackage($package->getName()); // select preferred package according to policy rules - if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) { + if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { $matchedPackage = $pool->literalToPackage($preferred[0]); } From 406a28708fbe60601df732523f7ecd2a029dfcd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=BDurek?= Date: Sun, 8 Dec 2019 02:38:22 +0100 Subject: [PATCH 165/321] updated phpstan to 0.12 --- .travis.yml | 8 +++-- phpstan/config.neon | 34 +++++-------------- src/Composer/Downloader/FileDownloader.php | 6 ++-- .../Installer/InstallationManager.php | 2 +- .../Repository/ComposerRepository.php | 2 +- src/Composer/Util/HttpDownloader.php | 2 +- 6 files changed, 19 insertions(+), 35 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0cc13f91a..49822707f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,9 @@ matrix: - php: 7.1 - php: 7.2 - php: 7.3 - env: PHPSTAN=1 + env: + - deps=high + - PHPSTAN=1 - php: 7.3 env: - deps=high @@ -64,8 +66,8 @@ script: - ls -d tests/Composer/Test/* | grep -v TestCase.php | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml --colors=always {} || (echo -e "\e[41mFAILED\e[0m {}" && exit 1);' # Run PHPStan - if [[ $PHPSTAN == "1" ]]; then - bin/composer require --dev phpstan/phpstan-shim:^0.11 --ignore-platform-reqs && - vendor/bin/phpstan.phar analyse --configuration=phpstan/config.neon; + bin/composer require --dev phpstan/phpstan:^0.12 && + vendor/bin/phpstan analyse --configuration=phpstan/config.neon; fi before_deploy: diff --git a/phpstan/config.neon b/phpstan/config.neon index ae53ac531..09b42ed27 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -1,24 +1,12 @@ parameters: autoload_files: - - phpstan/autoload.php + - autoload.php level: 0 excludes_analyse: - - 'tests/Composer/Test/Fixtures' - - 'tests/Composer/Test/Autoload/Fixtures' - - 'tests/Composer/Test/Plugin/Fixtures' + - '../tests/Composer/Test/Fixtures/*' + - '../tests/Composer/Test/Autoload/Fixtures/*' + - '../tests/Composer/Test/Plugin/Fixtures/*' ignoreErrors: - # unused parameters - - '~^Constructor of class Composer\\Repository\\VcsRepository has an unused parameter \$dispatcher\.$~' - - '~^Constructor of class Composer\\Repository\\PearRepository has an unused parameter \$dispatcher\.$~' - - '~^Constructor of class Composer\\Util\\Http\\CurlDownloader has an unused parameter \$disableTls\.$~' - - '~^Constructor of class Composer\\Util\\Http\\CurlDownloader has an unused parameter \$options\.$~' - - '~^Constructor of class Composer\\Repository\\PearRepository has an unused parameter \$config\.$~' - - # unused uses - - '~^Anonymous function has an unused use \$io\.$~' - - '~^Anonymous function has an unused use \$cache\.$~' - - '~^Anonymous function has an unused use \$path\.$~' - # ion cube is not installed - '~^Function ioncube_loader_\w+ not found\.$~' # rar is not installed @@ -33,14 +21,8 @@ parameters: # variable defined in eval - '~^Undefined variable: \$res$~' - # always checked whether the class exists - - '~^Instantiated class Symfony\\Component\\Console\\Terminal not found\.$~' - - '~^Class Symfony\\Component\\Console\\Input\\StreamableInputInterface not found\.$~' - - '~^Call to an undefined static method Symfony\\Component\\Process\\Process::fromShellCommandline\(\).$~' - - # parent call in test mocks - - '~^Composer\\Test\\Mock\\HttpDownloaderMock::__construct\(\) does not call parent constructor from Composer\\Util\\HttpDownloader\.$~' - - '~^Composer\\Test\\Mock\\InstallationManagerMock::__construct\(\) does not call parent constructor from Composer\\Installer\\InstallationManager\.$~' + # we don't have different constructors for parent/child + - '~^Unsafe usage of new static\(\)\.$~' paths: - - src - - tests + - ../src + - ../tests diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 7c7e45354..9775c27d9 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -113,7 +113,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $accept = null; $reject = null; - $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, $path, &$urls, &$accept, &$reject) { + $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject) { $url = reset($urls); if ($eventDispatcher) { @@ -160,7 +160,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface }); }; - $accept = function ($response) use ($io, $cache, $package, $fileName, $path, $self, &$urls) { + $accept = function ($response) use ($cache, $package, $fileName, $self, &$urls) { $url = reset($urls); $cacheKey = $url['cacheKey']; @@ -174,7 +174,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface return $fileName; }; - $reject = function ($e) use ($io, &$urls, $download, $fileName, $path, $package, &$retries, $filesystem, $self) { + $reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem, $self) { // clean up if (file_exists($fileName)) { $filesystem->unlink($fileName); diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 9c3fb6884..8d1579b63 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -246,7 +246,7 @@ class InstallationManager $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); } }, function ($e) use ($jobType, $installer, $package, $initialPackage, $loop, $io) { - $this->io->writeError(' ' . ucfirst($jobType) .' of '.$package->getPrettyName().' failed'); + $io->writeError(' ' . ucfirst($jobType) .' of '.$package->getPrettyName().' failed'); $promise = $installer->cleanup($jobType, $package, $initialPackage); if ($promise) { diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e4cfbba83..02f2a05fe 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1119,7 +1119,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $data; }; - $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, $cache, &$degradedMode) { + $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, &$degradedMode) { if ($e instanceof TransportException && $e->getStatusCode() === 404) { return false; } diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index c4b6c5424..8a117a232 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -190,7 +190,7 @@ class HttpDownloader $downloader->scheduleNextJob(); return $response; - }, function ($e) use ($io, &$job, $downloader) { + }, function ($e) use (&$job, $downloader) { $job['status'] = HttpDownloader::STATUS_FAILED; $job['exception'] = $e; From e5b13b4c8f5fd342d2749b90a3be6067544a3b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=BDurek?= Date: Tue, 10 Dec 2019 22:31:07 +0100 Subject: [PATCH 166/321] Added PHPStan rule that checks we don't use $this in closures. --- .gitattributes | 1 + composer.json | 9 +++- .../src/AnonymousFunctionWithThisRule.php | 46 +++++++++++++++++++ .../AnonymousFunctionWithThisRuleTest.php | 28 +++++++++++ phpstan/Rules/tests/data/method-with-this.php | 34 ++++++++++++++ phpstan/config.neon | 9 ++++ 6 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 phpstan/Rules/src/AnonymousFunctionWithThisRule.php create mode 100644 phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php create mode 100644 phpstan/Rules/tests/data/method-with-this.php diff --git a/.gitattributes b/.gitattributes index 51b431136..9cd1bb7da 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,4 @@ .travis.yml export-ignore appveyor.yml export-ignore phpunit.xml.dist export-ignore +/phpstan/ export-ignore diff --git a/composer.json b/composer.json index d1f98a56a..435aa5c01 100644 --- a/composer.json +++ b/composer.json @@ -66,8 +66,13 @@ }, "autoload-dev": { "psr-4": { - "Composer\\Test\\": "tests/Composer/Test" - } + "Composer\\Test\\": "tests/Composer/Test", + "Composer\\PHPStanRules\\": "phpstan/Rules/src", + "Composer\\PHPStanRulesTests\\": "phpstan/Rules/tests" + }, + "classmap": [ + "phpstan/Rules/tests/data" + ] }, "bin": [ "bin/composer" diff --git a/phpstan/Rules/src/AnonymousFunctionWithThisRule.php b/phpstan/Rules/src/AnonymousFunctionWithThisRule.php new file mode 100644 index 000000000..d9a44ecb5 --- /dev/null +++ b/phpstan/Rules/src/AnonymousFunctionWithThisRule.php @@ -0,0 +1,46 @@ + + */ +final class AnonymousFunctionWithThisRule implements Rule +{ + /** + * @inheritDoc + */ + public function getNodeType(): string + { + return \PhpParser\Node\Expr\Variable::class; + } + + /** + * @inheritDoc + */ + public function processNode(Node $node, Scope $scope): array + { + if (!\is_string($node->name) || $node->name !== 'this') { + return []; + } + + if ($scope->isInClosureBind()) { + return []; + } + + if (!$scope->isInClass()) { + // reported in other standard rule on level 0 + return []; + } + + if ($scope->isInAnonymousFunction()) { + return ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.']; + } + + return []; + } +} diff --git a/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php b/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php new file mode 100644 index 000000000..add285d17 --- /dev/null +++ b/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php @@ -0,0 +1,28 @@ + + */ +final class AnonymousFunctionWithThisRuleTest extends RuleTestCase +{ + /** + * @inheritDoc + */ + protected function getRule(): \PHPStan\Rules\Rule + { + return new AnonymousFunctionWithThisRule(); + } + + public function testWithThis(): void + { + $this->analyse([__DIR__ . '/data/method-with-this.php'], [ + ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 13], + ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 17], + ]); + } +} diff --git a/phpstan/Rules/tests/data/method-with-this.php b/phpstan/Rules/tests/data/method-with-this.php new file mode 100644 index 000000000..e0b15bffa --- /dev/null +++ b/phpstan/Rules/tests/data/method-with-this.php @@ -0,0 +1,34 @@ +firstProp; + }; + + call_user_func(function() { + $this->funMethod(); + }, $this); + + $bind = 'bind'; + function() use($bind) { + + }; + } +} + +function global_ok() { + $_SERVER['REMOTE_ADDR']; +} + +function global_this() { + // not checked by our rule, it is checked with standard phpstan rule on level 0 + $this['REMOTE_ADDR']; +} diff --git a/phpstan/config.neon b/phpstan/config.neon index 09b42ed27..599d27d51 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -23,6 +23,15 @@ parameters: # we don't have different constructors for parent/child - '~^Unsafe usage of new static\(\)\.$~' + + # hhvm should have support for $this in closures + - + count: 1 + message: '~^Using \$this inside anonymous function is prohibited because of PHP 5\.3 support\.$~' + path: '../tests/Composer/Test/Repository/PlatformRepositoryTest.php' paths: - ../src - ../tests + +rules: + - Composer\PHPStanRules\AnonymousFunctionWithThisRule From a6176a7beb8791d464131e3e7f7102afb7e649e2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 13 Jan 2020 13:36:09 +0100 Subject: [PATCH 167/321] Add IOInterface methods --- src/Composer/IO/IOInterface.php | 18 ++++++++++++++++++ src/Composer/Util/ProcessExecutor.php | 14 +++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Composer/IO/IOInterface.php b/src/Composer/IO/IOInterface.php index 95f891c57..0ba1cf932 100644 --- a/src/Composer/IO/IOInterface.php +++ b/src/Composer/IO/IOInterface.php @@ -81,6 +81,24 @@ interface IOInterface extends LoggerInterface */ public function writeError($messages, $newline = true, $verbosity = self::NORMAL); + /** + * Writes a message to the output, without formatting it. + * + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline or not + * @param int $verbosity Verbosity level from the VERBOSITY_* constants + */ + public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL); + + /** + * Writes a message to the error output, without formatting it. + * + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline or not + * @param int $verbosity Verbosity level from the VERBOSITY_* constants + */ + public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL); + /** * Overwrites a previous message to the output. * diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 83f19cf2d..c16953652 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -112,18 +112,10 @@ class ProcessExecutor return; } - if (method_exists($this->io, 'writeRaw')) { - if (Process::ERR === $type) { - $this->io->writeErrorRaw($buffer, false); - } else { - $this->io->writeRaw($buffer, false); - } + if (Process::ERR === $type) { + $this->io->writeErrorRaw($buffer, false); } else { - if (Process::ERR === $type) { - $this->io->writeError($buffer, false); - } else { - $this->io->write($buffer, false); - } + $this->io->writeRaw($buffer, false); } } From f91859ceff13c6c75e05614784364926c0b53a15 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 14 Jan 2020 15:46:58 +0100 Subject: [PATCH 168/321] Fix expectation --- tests/Composer/Test/EventDispatcher/EventDispatcherTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 804008259..43d4781a4 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -452,7 +452,7 @@ class EventDispatcherTest extends TestCase ->with($this->equalTo('> echo foo')); $io->expects($this->once()) - ->method('write') + ->method('writeRaw') ->with($this->equalTo('foo'.PHP_EOL), false); $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false); From 40f5806a7ca17f8798a8ccd986f02eca31f75e1e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 14 Jan 2020 16:20:31 +0100 Subject: [PATCH 169/321] Fix ComposerRepo issue --- .../Repository/ComposerRepository.php | 21 +++++++++--------- .../Repository/ComposerRepositoryTest.php | 22 +++++++++---------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 8ff6da589..975282702 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -242,11 +242,15 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return array_values($this->loadAsyncPackages($packageMap)); } - throw new \LogicException('Composer repositories that have lazy providers and no available-packages list can not load the complete list of packages, use getProviderNames instead.'); + if ($this->hasPartialPackages()) { + return array_values($this->partialPackagesByName); + } + + throw new \LogicException('Composer repositories that have lazy providers and no available-packages list can not load the complete list of packages, use getPackageNames instead.'); } if ($hasProviders) { - throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getProviderNames instead.'); + throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getPackageNames instead.'); } return parent::getPackages(); @@ -263,6 +267,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } // TODO implement new list API endpoint for those repos somehow? + + if ($this->hasPartialPackages()) { + return array_keys($this->partialPackagesByName); + } + return array(); } @@ -391,14 +400,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->loadProviderListings($this->loadRootServerFile()); } - if ($this->hasPartialPackages) { - if (null === $this->partialPackagesByName) { - $this->initializePartialPackages(); - } - - return array_keys($this->partialPackagesByName); - } - if ($this->lazyProvidersUrl) { // Can not determine list of provided packages for lazy repositories return array(); diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 313f8b7b6..1915389c4 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -286,30 +286,28 @@ class ComposerRepositoryTest extends TestCase public function testGetProviderNamesWillReturnPartialPackageNames() { - $rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') ->disableOriginalConstructor() ->getMock(); - $rfs->expects($this->at(0)) - ->method('getContents') - ->with('example.org', 'http://example.org/packages.json', false) - ->willReturn(json_encode(array( + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($url = 'http://example.org/packages.json') + ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array( 'providers-lazy-url' => '/foo/p/%package%.json', 'packages' => array('foo/bar' => array( - 'dev-branch' => array(), - 'v1.0.0' => array(), + 'dev-branch' => array('name' => 'foo/bar'), + 'v1.0.0' => array('name' => 'foo/bar'), )) - ))); + )))); $repository = new ComposerRepository( array('url' => 'http://example.org/packages.json'), new NullIO(), FactoryMock::createConfig(), - null, - $rfs + $httpDownloader ); - $this->assertTrue($repository->hasProviders()); - $this->assertEquals(array('foo/bar'), $repository->getProviderNames()); + $this->assertEquals(array('foo/bar'), $repository->getPackageNames()); } } From 4a7d42604fe2f0f0bb34f3e6ee9e17f44476f3dc Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 14 Jan 2020 16:27:16 +0100 Subject: [PATCH 170/321] Fix tests --- .../Test/Repository/Vcs/GitHubDriverTest.php | 31 ++++++++++++------- .../Test/Util/ProcessExecutorTest.php | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index 4115b65da..8af7b73b7 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -234,25 +234,34 @@ class GitHubDriverTest extends TestCase ->method('isInteractive') ->will($this->returnValue(true)); - $remoteFilesystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem') - ->setConstructorArgs(array($io)) + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor') + ->disableOriginalConstructor() ->getMock(); - $remoteFilesystem->expects($this->at(0)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}')); + $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader') + ->setConstructorArgs(array($io, $this->config)) + ->getMock(); - $remoteFilesystem->expects($this->at(1)) - ->method('getContents') - ->with($this->equalTo('github.com'), $this->equalTo($composerJsonUrl), $this->equalTo(false)) - ->will($this->returnValue('{"encoding": "base64", "content": "' . base64_encode('{"name": "composer/packagist"}') . '"}')); + $httpDownloader->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($repoApiUrl)) + ->will($this->returnValue(new Response(array('url' => $repoApiUrl), 200, array(), '{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}'))); + + $httpDownloader->expects($this->at(1)) + ->method('get') + ->with($this->equalTo($composerJsonUrl)) + ->will($this->returnValue(new Response(array('url' => $composerJsonUrl), 200, array(), '{"encoding": "base64", "content": "' . base64_encode('{"name": "composer/packagist"}') . '"}'))); + + $httpDownloader->expects($this->at(2)) + ->method('get') + ->with($this->equalTo($url = 'https://api.github.com/repos/composer/packagist/commits/'.$sha)) + ->will($this->returnValue(new Response(array('url' => $url), 200, array(), '{"commit": {"committer":{ "date": "2012-09-10"}}}'))); $repoConfig = array( 'url' => $repoUrl, ); - $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $remoteFilesystem); + $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', array($identifier => $sha)); diff --git a/tests/Composer/Test/Util/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php index 63ed0b7de..42fea21f6 100644 --- a/tests/Composer/Test/Util/ProcessExecutorTest.php +++ b/tests/Composer/Test/Util/ProcessExecutorTest.php @@ -44,7 +44,7 @@ class ProcessExecutorTest extends TestCase { $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $io->expects($this->once()) - ->method('write') + ->method('writeRaw') ->with($this->equalTo('foo'.PHP_EOL), false); $process = new ProcessExecutor($io); From 47a94b3a88a22c155cd8832fc681a9071dbfa0c5 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 12:02:12 +0100 Subject: [PATCH 171/321] Ensure packages that exist in a higher prio repo never get loaded in lower prio repos, fixes #5076 --- .../DependencyResolver/PoolBuilder.php | 9 +++- src/Composer/Repository/BaseRepository.php | 22 ++++++---- .../Repository/ComposerRepository.php | 24 +++++++---- .../Repository/RepositoryInterface.php | 2 +- .../installer/repositories-priorities.test | 42 +++++++++++++++++++ .../installer/repositories-priorities2.test | 26 ++++++++++++ 6 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/repositories-priorities.test create mode 100644 tests/Composer/Test/Fixtures/installer/repositories-priorities2.test diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 7b80c41bd..c57e4fa43 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -97,9 +97,14 @@ class PoolBuilder } // TODO should we really pass the callable into here? - $packages = $repository->loadPackages($loadNames, $this->isPackageAcceptableCallable); + $result = $repository->loadPackages($loadNames, $this->isPackageAcceptableCallable); + + foreach ($result['namesFound'] as $name) { + // avoid loading the same package again from other repositories once it has been found + unset($loadNames[$name]); + } + foreach ($result['packages'] as $package) { - foreach ($packages as $package) { if (call_user_func($this->isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { $newLoadNames += $this->loadPackage($request, $package, $key); } diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/BaseRepository.php index ddf8b6e73..dfd99d7c7 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/BaseRepository.php @@ -31,16 +31,20 @@ abstract class BaseRepository implements RepositoryInterface $packages = $this->getPackages(); $result = array(); + $namesFound = array(); foreach ($packages as $package) { - if ( - array_key_exists($package->getName(), $packageMap) - && (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) - && call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability()) - ) { - $result[spl_object_hash($package)] = $package; - if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { - $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + if (array_key_exists($package->getName(), $packageMap)) { + if ( + (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) + && call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability()) + ) { + $result[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { + $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + } } + + $namesFound[$package->getName()] = true; } } @@ -52,7 +56,7 @@ abstract class BaseRepository implements RepositoryInterface } } - return $result; + return array('namesFound' => array_keys($namesFound), 'packages' => $result); } /** diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 975282702..0a9b7d5e4 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -143,7 +143,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $packages = $this->loadAsyncPackages(array($name => $constraint)); - return reset($packages); + return reset($packages['packages']); } if ($hasProviders) { @@ -181,7 +181,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return array(); } - return $this->loadAsyncPackages(array($name => $constraint)); + $result = $this->loadAsyncPackages(array($name => $constraint)); + + return $result['packages']; } if ($hasProviders) { @@ -239,7 +241,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $packageMap[$name] = new EmptyConstraint(); } - return array_values($this->loadAsyncPackages($packageMap)); + $result = $this->loadAsyncPackages($packageMap); + + return array_values($result['packages']); } if ($this->hasPartialPackages()) { @@ -297,6 +301,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $packages = array(); + $namesFound = array(); if ($hasProviders || $this->hasPartialPackages()) { foreach ($packageNameMap as $name => $constraint) { @@ -313,6 +318,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($candidate->getName() !== $name) { throw new \LogicException('whatProvides should never return a package with a different name than the requested one'); } + $namesFound[$name] = true; if (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { $matches[spl_object_hash($candidate)] = $candidate; if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) { @@ -343,10 +349,12 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito }, ARRAY_FILTER_USE_KEY); } - $packages = array_merge($packages, $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable)); + $result = $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable); + $packages = array_merge($packages, $result['packages']); + $namesFound = array_merge($namesFound, $result['namesFound']); } - return $packages; + return array('namesFound' => array_keys($namesFound), 'packages' => $packages); } /** @@ -586,6 +594,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->loadRootServerFile(); $packages = array(); + $namesFound = array(); $promises = array(); $repo = $this; @@ -619,7 +628,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified) - ->then(function ($response) use (&$packages, $contents, $realName, $constraint, $repo, $isPackageAcceptableCallable) { + ->then(function ($response) use (&$packages, &$namesFound, $contents, $realName, $constraint, $repo, $isPackageAcceptableCallable) { if (true === $response) { $response = $contents; } @@ -657,6 +666,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito unset($expanded, $expandedVersion, $versionData); } + $namesFound[$realName] = true; $versionsToLoad = array(); foreach ($versions as $version) { if (!isset($version['version_normalized'])) { @@ -683,7 +693,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->loop->wait($promises); - return $packages; + return array('namesFound' => $namesFound, 'packages' => $packages); // RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed } diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index e5f2c5159..f5e80c24c 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -69,7 +69,7 @@ interface RepositoryInterface extends \Countable * * @param ConstraintInterface[] $packageNameMap package names pointing to constraints * @param $isPackageAcceptableCallable - * @return PackageInterface[] + * @return array [namesFound => string[], packages => PackageInterface[]] */ public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable); diff --git a/tests/Composer/Test/Fixtures/installer/repositories-priorities.test b/tests/Composer/Test/Fixtures/installer/repositories-priorities.test new file mode 100644 index 000000000..42f7a1a6e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/repositories-priorities.test @@ -0,0 +1,42 @@ +--TEST-- +Packages found in a higher priority repository take precedence even if they are not found in the requested version +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "1.0.0" } + ] + }, + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "2.0.0" } + ] + } + ], + "require": { + "foo/a": "2.*" + } +} +--RUN-- +update +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - The requested package foo/a could not be found in any version, there may be a typo in the package name. + +Potential causes: + - A typo in the package name + - The package is not available in a stable-enough version according to your minimum-stability setting + see for more details. + - It's a private package and you forgot to add a custom repository to find it + +Read for further common problems. +--EXPECT-- +--EXPECT-EXIT-CODE-- +2 diff --git a/tests/Composer/Test/Fixtures/installer/repositories-priorities2.test b/tests/Composer/Test/Fixtures/installer/repositories-priorities2.test new file mode 100644 index 000000000..598079d80 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/repositories-priorities2.test @@ -0,0 +1,26 @@ +--TEST-- +Packages found in a higher priority repository take precedence +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "1.0.0" } + ] + }, + { + "type": "package", + "package": [ + { "name": "foo/a", "version": "1.1.0" } + ] + } + ], + "require": { + "foo/a": "1.*" + } +} +--RUN-- +update +--EXPECT-- +Installing foo/a (1.0.0) From 926afab1f4e937ddaa3ca22c79d55743069261b8 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 12:37:24 +0100 Subject: [PATCH 172/321] Move loadPackages impl out of BaseRepository --- src/Composer/Repository/ArrayRepository.php | 40 ++++++++++++++++++- src/Composer/Repository/BaseRepository.php | 34 ---------------- .../Repository/CompositeRepository.php | 20 ++++++++++ 3 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index c11c7fe68..42e9d50e1 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -28,8 +28,8 @@ class ArrayRepository extends BaseRepository { /** @var PackageInterface[] */ protected $packages; - - /** + + /** * @var PackageInterface[] indexed by package unique name and used to cache hasPackage calls */ protected $packageMap; @@ -41,6 +41,42 @@ class ArrayRepository extends BaseRepository } } + /** + * {@inheritDoc} + */ + public function loadPackages(array $packageMap, $isPackageAcceptableCallable) + { + $packages = $this->getPackages(); + + $result = array(); + $namesFound = array(); + foreach ($packages as $package) { + if (array_key_exists($package->getName(), $packageMap)) { + if ( + (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) + && call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability()) + ) { + $result[spl_object_hash($package)] = $package; + if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { + $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + } + } + + $namesFound[$package->getName()] = true; + } + } + + foreach ($packages as $package) { + if ($package instanceof AliasPackage) { + if (isset($result[spl_object_hash($package->getAliasOf())])) { + $result[spl_object_hash($package)] = $package; + } + } + } + + return array('namesFound' => array_keys($namesFound), 'packages' => $result); + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/BaseRepository.php index dfd99d7c7..1274dbb43 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/BaseRepository.php @@ -25,40 +25,6 @@ use Composer\Package\Link; */ abstract class BaseRepository implements RepositoryInterface { - // TODO should this stay here? some repos need a better implementation - public function loadPackages(array $packageMap, $isPackageAcceptableCallable) - { - $packages = $this->getPackages(); - - $result = array(); - $namesFound = array(); - foreach ($packages as $package) { - if (array_key_exists($package->getName(), $packageMap)) { - if ( - (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) - && call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability()) - ) { - $result[spl_object_hash($package)] = $package; - if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { - $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); - } - } - - $namesFound[$package->getName()] = true; - } - } - - foreach ($packages as $package) { - if ($package instanceof AliasPackage) { - if (isset($result[spl_object_hash($package->getAliasOf())])) { - $result[spl_object_hash($package)] = $package; - } - } - } - - return array('namesFound' => array_keys($namesFound), 'packages' => $result); - } - /** * Returns a list of links causing the requested needle packages to be installed, as an associative array with the * dependent's name as key, and an array containing in order the PackageInterface and Link describing the relationship diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index ce57504f0..ddaa94694 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -94,6 +94,26 @@ class CompositeRepository extends BaseRepository return $packages ? call_user_func_array('array_merge', $packages) : array(); } + /** + * {@inheritDoc} + */ + public function loadPackages(array $packageMap, $isPackageAcceptableCallable) + { + $packages = array(); + $namesFound = array(); + foreach ($this->repositories as $repository) { + /* @var $repository RepositoryInterface */ + $result = $repository->findPackages($name, $constraint); + $packages[] = $result['packages']; + $namesFound[] = $result['namesFound']; + } + + return array( + 'packages' => $packages ? call_user_func_array('array_merge', $packages) : array(), + 'namesFound' => $namesFound ? array_unique(call_user_func_array('array_merge', $namesFound)) : array(), + ); + } + /** * {@inheritdoc} */ From 30b6a410359d4b76d45796ab1d01776fbed98785 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 12:58:30 +0100 Subject: [PATCH 173/321] Extract MetadataMinifier util --- .../Repository/ComposerRepository.php | 26 +------ src/Composer/Util/MetadataMinifier.php | 78 +++++++++++++++++++ .../Test/Util/MetadataMinifierTest.php | 45 +++++++++++ 3 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 src/Composer/Util/MetadataMinifier.php create mode 100644 tests/Composer/Test/Util/MetadataMinifierTest.php diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 975282702..fe2a8787b 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -32,6 +32,7 @@ use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; use Composer\Util\Http\Response; +use Composer\Util\MetadataMinifier; /** * @author Jordi Boggiano @@ -631,30 +632,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $versions = $response['packages'][$realName]; if (isset($response['minified']) && $response['minified'] === 'composer/2.0') { - // TODO extract in other method - $expanded = array(); - $expandedVersion = null; - foreach ($versions as $versionData) { - if (!$expandedVersion) { - $expandedVersion = $versionData; - $expanded[] = $expandedVersion; - continue; - } - - // add any changes from the previous version to the expanded one - foreach ($versionData as $key => $val) { - if ($val === '__unset') { - unset($expandedVersion[$key]); - } else { - $expandedVersion[$key] = $val; - } - } - - $expanded[] = $expandedVersion; - } - - $versions = $expanded; - unset($expanded, $expandedVersion, $versionData); + $versions = MetadataMinifier::expand($versions); } $versionsToLoad = array(); diff --git a/src/Composer/Util/MetadataMinifier.php b/src/Composer/Util/MetadataMinifier.php new file mode 100644 index 000000000..669dab54b --- /dev/null +++ b/src/Composer/Util/MetadataMinifier.php @@ -0,0 +1,78 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +class MetadataMinifier +{ + public static function expand(array $versions) + { + $expanded = array(); + $expandedVersion = null; + foreach ($versions as $versionData) { + if (!$expandedVersion) { + $expandedVersion = $versionData; + $expanded[] = $expandedVersion; + continue; + } + + // add any changes from the previous version to the expanded one + foreach ($versionData as $key => $val) { + if ($val === '__unset') { + unset($expandedVersion[$key]); + } else { + $expandedVersion[$key] = $val; + } + } + + $expanded[] = $expandedVersion; + } + + return $expanded; + } + + public static function minify(array $versions) + { + $minifiedVersions = array(); + + $lastKnownVersionData = null; + foreach ($versions as $version) { + if (!$lastKnownVersionData) { + $lastKnownVersionData = $version; + $minifiedVersions[] = $version; + continue; + } + + $minifiedVersion = []; + + // add any changes from the previous version + foreach ($version as $key => $val) { + if (!isset($lastKnownVersionData[$key]) || $lastKnownVersionData[$key] !== $val) { + $minifiedVersion[$key] = $val; + $lastKnownVersionData[$key] = $val; + } + } + + // store any deletions from the previous version for keys missing in current one + foreach ($lastKnownVersionData as $key => $val) { + if (!isset($version[$key])) { + $minifiedVersion[$key] = "__unset"; + unset($lastKnownVersionData[$key]); + } + } + + $minifiedVersions[] = $minifiedVersion; + } + + return $minifiedVersions; + } +} diff --git a/tests/Composer/Test/Util/MetadataMinifierTest.php b/tests/Composer/Test/Util/MetadataMinifierTest.php new file mode 100644 index 000000000..ad8d3abad --- /dev/null +++ b/tests/Composer/Test/Util/MetadataMinifierTest.php @@ -0,0 +1,45 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\MetadataMinifier; +use Composer\Package\CompletePackage; +use Composer\Package\Dumper\ArrayDumper; +use PHPUnit\Framework\TestCase; + +class MetadataMinifierTest extends TestCase +{ + public function testMinifyExpand() + { + $package1 = new CompletePackage('foo/bar', '2.0.0.0', '2.0.0'); + $package1->setScripts(array('foo' => 'bar')); + $package1->setLicense(array('MIT')); + $package2 = new CompletePackage('foo/bar', '1.2.0.0', '1.2.0'); + $package2->setLicense(array('GPL')); + $package2->setHomepage('https://example.org'); + $package3 = new CompletePackage('foo/bar', '1.0.0.0', '1.0.0'); + $package3->setLicense(array('GPL')); + $dumper = new ArrayDumper(); + + $minified = array( + array('name' => 'foo/bar', 'version' => '2.0.0', 'version_normalized' => '2.0.0.0', 'type' => 'library', 'scripts' => array('foo' => 'bar'), 'license' => array('MIT')), + array('version' => '1.2.0', 'version_normalized' => '1.2.0.0', 'license' => array('GPL'), 'homepage' => 'https://example.org', 'scripts' => '__unset'), + array('version' => '1.0.0', 'version_normalized' => '1.0.0.0', 'homepage' => '__unset'), + ); + + $source = array($dumper->dump($package1), $dumper->dump($package2), $dumper->dump($package3)); + + $this->assertSame($minified, MetadataMinifier::minify($source)); + $this->assertSame($source, MetadataMinifier::expand($minified)); + } +} From f68731e6631f9a17ff011cf2d53abfeb9d04f81f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 14:52:44 +0100 Subject: [PATCH 174/321] Remove package/repo priority concept as it is enforced by the pool builder now --- .../DependencyResolver/DefaultPolicy.php | 105 ++++++------------ src/Composer/DependencyResolver/Pool.php | 9 +- .../DependencyResolver/PoolBuilder.php | 8 +- src/Composer/Repository/RepositorySet.php | 14 +-- .../DependencyResolver/DefaultPolicyTest.php | 43 +------ 5 files changed, 46 insertions(+), 133 deletions(-) diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 7d241b482..0b978ec59 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -69,7 +69,6 @@ class DefaultPolicy implements PolicyInterface } foreach ($packages as &$sortedLiterals) { - $sortedLiterals = $this->pruneToHighestPriority($pool, $sortedLiterals); $sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals); $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals); } @@ -104,51 +103,47 @@ class DefaultPolicy implements PolicyInterface */ public function compareByPriority(Pool $pool, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false) { - if ($a->getRepository() === $b->getRepository()) { - // prefer aliases to the original package - if ($a->getName() === $b->getName()) { - $aAliased = $a instanceof AliasPackage; - $bAliased = $b instanceof AliasPackage; - if ($aAliased && !$bAliased) { - return -1; // use a - } - if (!$aAliased && $bAliased) { - return 1; // use b - } + // prefer aliases to the original package + if ($a->getName() === $b->getName()) { + $aAliased = $a instanceof AliasPackage; + $bAliased = $b instanceof AliasPackage; + if ($aAliased && !$bAliased) { + return -1; // use a } - - if (!$ignoreReplace) { - // return original, not replaced - if ($this->replaces($a, $b)) { - return 1; // use b - } - if ($this->replaces($b, $a)) { - return -1; // use a - } - - // for replacers not replacing each other, put a higher prio on replacing - // packages with the same vendor as the required package - if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) { - $requiredVendor = substr($requiredPackage, 0, $pos); - - $aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor; - $bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor; - - if ($bIsSameVendor !== $aIsSameVendor) { - return $aIsSameVendor ? -1 : 1; - } - } + if (!$aAliased && $bAliased) { + return 1; // use b } - - // priority equal, sort by package id to make reproducible - if ($a->id === $b->id) { - return 0; - } - - return ($a->id < $b->id) ? -1 : 1; } - return ($pool->getPriority($a->id) > $pool->getPriority($b->id)) ? -1 : 1; + if (!$ignoreReplace) { + // return original, not replaced + if ($this->replaces($a, $b)) { + return 1; // use b + } + if ($this->replaces($b, $a)) { + return -1; // use a + } + + // for replacers not replacing each other, put a higher prio on replacing + // packages with the same vendor as the required package + if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) { + $requiredVendor = substr($requiredPackage, 0, $pos); + + $aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor; + $bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor; + + if ($bIsSameVendor !== $aIsSameVendor) { + return $aIsSameVendor ? -1 : 1; + } + } + } + + // priority equal, sort by package id to make reproducible + if ($a->id === $b->id) { + return 0; + } + + return ($a->id < $b->id) ? -1 : 1; } /** @@ -198,32 +193,6 @@ class DefaultPolicy implements PolicyInterface return $bestLiterals; } - /** - * Assumes that highest priority packages come first - */ - protected function pruneToHighestPriority(Pool $pool, array $literals) - { - $selected = array(); - - $priority = null; - - foreach ($literals as $literal) { - $package = $pool->literalToPackage($literal); - - if (null === $priority) { - $priority = $pool->getPriority($package->id); - } - - if ($pool->getPriority($package->id) != $priority) { - break; - } - - $selected[] = $literal; - } - - return $selected; - } - /** * Assumes that locally aliased (in root package requires) packages take priority over branch-alias ones * diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 9470c6e35..ffb70c300 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -36,7 +36,6 @@ class Pool implements \Countable protected $packages = array(); protected $packageByName = array(); protected $packageByExactName = array(); - protected $priorities = array(); protected $versionParser; protected $providerCache = array(); @@ -45,13 +44,12 @@ class Pool implements \Countable $this->versionParser = new VersionParser; } - public function setPackages(array $packages, array $priorities = array()) + public function setPackages(array $packages) { $id = 1; foreach ($packages as $i => $package) { $this->packages[] = $package; - $this->priorities[] = isset($priorities[$i]) ? $priorities[$i] : 0; $package->id = $id++; $names = $package->getNames(); @@ -63,11 +61,6 @@ class Pool implements \Countable } } - public function getPriority($id) - { - return $this->priorities[$id - 1]; - } - /** * Retrieves the package object for a given package id. * diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index c57e4fa43..05d029604 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -38,7 +38,6 @@ class PoolBuilder private $loadedNames = array(); private $packages = array(); - private $priorities = array(); public function __construct($isPackageAcceptableCallable, array $rootRequires = array()) { @@ -134,7 +133,6 @@ class PoolBuilder if (!$found) { foreach ($aliasedPackages as $index => $packageOrAlias) { unset($this->packages[$index]); - unset($this->priorities[$index]); } } } @@ -149,7 +147,7 @@ class PoolBuilder } } - $pool->setPackages($this->packages, $this->priorities); + $pool->setPackages($this->packages); unset($this->aliasMap); unset($this->loadedNames); @@ -158,11 +156,10 @@ class PoolBuilder return $pool; } - private function loadPackage(Request $request, PackageInterface $package, $repoIndex) + private function loadPackage(Request $request, PackageInterface $package) { $index = count($this->packages); $this->packages[] = $package; - $this->priorities[] = -$repoIndex; if ($package instanceof AliasPackage) { $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package; @@ -192,7 +189,6 @@ class PoolBuilder $package->getRepository()->addPackage($aliasPackage); // TODO do we need this? $this->packages[] = $aliasPackage; - $this->priorities[] = -$repoIndex; $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$index+1] = $aliasPackage; } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 21526b96f..6ed647297 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -65,6 +65,9 @@ class RepositorySet /** * Adds a repository to this repository set * + * The first repos added have a higher priority. As soon as a package is found in any + * repository the search for that package ends, and following repos will not be consulted. + * * @param RepositoryInterface $repo A package repository */ public function addRepository(RepositoryInterface $repo) @@ -134,17 +137,6 @@ class RepositorySet return $candidates; } - public function getPriority(RepositoryInterface $repo) - { - $priority = array_search($repo, $this->repositories, true); - - if (false === $priority) { - throw new \RuntimeException("Could not determine repository priority. The repository was not registered in the pool."); - } - - return -$priority; - } - /** * Create a pool for dependency resolution from the packages in this repository set. * diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index b4df12310..827a9181d 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\DependencyResolver; use Composer\Repository\ArrayRepository; +use Composer\Repository\LockArrayRepository; use Composer\Repository\RepositoryInterface; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\Pool; @@ -28,7 +29,7 @@ class DefaultPolicyTest extends TestCase protected $repositorySet; /** @var ArrayRepository */ protected $repo; - /** @var ArrayRepository */ + /** @var LockArrayRepository */ protected $repoLocked; /** @var DefaultPolicy */ protected $policy; @@ -37,7 +38,7 @@ class DefaultPolicyTest extends TestCase { $this->repositorySet = new RepositorySet(array(), array(), 'dev'); $this->repo = new ArrayRepository; - $this->repoLocked = new ArrayRepository; + $this->repoLocked = new LockArrayRepository; $this->policy = new DefaultPolicy; } @@ -122,44 +123,6 @@ class DefaultPolicyTest extends TestCase $this->assertSame($expected, $selected); } - public function testSelectNewestOverLocked() - { - $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); - $this->repoLocked->addPackage($packageAInstalled = $this->getPackage('A', '1.0')); - $this->repositorySet->addRepository($this->repo); - $this->repositorySet->addRepository($this->repoLocked); - - $pool = $this->repositorySet->createPoolForPackage('A'); - - $literals = array($packageA->getId(), $packageAInstalled->getId()); - $expected = array($packageA->getId()); - - $selected = $this->policy->selectPreferredPackages($pool, $literals); - - $this->assertSame($expected, $selected); - } - - public function testSelectFirstRepo() - { - $otherRepository = new ArrayRepository; - - $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); - $otherRepository->addPackage($packageAImportant = $this->getPackage('A', '1.0')); - - $this->repositorySet->addRepository($otherRepository); - $this->repositorySet->addRepository($this->repo); - $this->repositorySet->addRepository($this->repoLocked); - - $pool = $this->repositorySet->createPoolForPackage('A'); - - $literals = array($packageA->getId(), $packageAImportant->getId()); - $expected = array($packageAImportant->getId()); - - $selected = $this->policy->selectPreferredPackages($pool, $literals); - - $this->assertSame($expected, $selected); - } - public function testRepositoryOrderingAffectsPriority() { $repo1 = new ArrayRepository; From dd556f989b8a6d6605454c2862d6c8856d18bcbf Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 14:56:58 +0100 Subject: [PATCH 175/321] Use LockArrayRepository instead of RepositoryInterface to clearly indicate which type of repo it is in some places --- src/Composer/DependencyResolver/Request.php | 4 +-- src/Composer/Package/Locker.php | 4 +-- src/Composer/Repository/RepositorySet.php | 10 ++++--- .../DependencyResolver/DefaultPolicyTest.php | 26 +++++++++---------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 0656cbbae..d0502c1c5 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -15,7 +15,7 @@ namespace Composer\DependencyResolver; use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Package\RootAliasPackage; -use Composer\Repository\RepositoryInterface; +use Composer\Repository\LockArrayRepository; use Composer\Semver\Constraint\ConstraintInterface; /** @@ -28,7 +28,7 @@ class Request protected $fixedPackages = array(); protected $unlockables = array(); - public function __construct(RepositoryInterface $lockedRepository = null) + public function __construct(LockArrayRepository $lockedRepository = null) { $this->lockedRepository = $lockedRepository; } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 8069ba50c..1e5ea8531 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -150,7 +150,7 @@ class Locker * * @param bool $withDevReqs true to retrieve the locked dev packages * @throws \RuntimeException - * @return \Composer\Repository\RepositoryInterface + * @return \Composer\Repository\LockArrayRepository */ public function getLockedRepository($withDevReqs = false) { @@ -194,7 +194,7 @@ class Locker return $packages; } - throw new \RuntimeException('Your composer.lock was created before 2012-09-15, and is not supported anymore. Run "composer update" to generate a new one.'); + throw new \RuntimeException('Your composer.lock is invalid. Run "composer update" to generate a new one.'); } /** diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 6ed647297..9b15a0115 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -19,6 +19,7 @@ use Composer\Package\BasePackage; use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; +use Composer\Repository\LockArrayRepository; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Test\DependencyResolver\PoolTest; @@ -150,14 +151,15 @@ class RepositorySet } // TODO unify this with above in some simpler version without "request"? - public function createPoolForPackage($packageName) + public function createPoolForPackage($packageName, LockArrayRepository $lockedRepo = null) { - return $this->createPoolForPackages(array($packageName)); + return $this->createPoolForPackages(array($packageName), $lockedRepo); } - public function createPoolForPackages($packageNames) + public function createPoolForPackages($packageNames, LockArrayRepository $lockedRepo = null) { - $request = new Request(); + $request = new Request($lockedRepo); + foreach ($packageNames as $packageName) { $request->install($packageName); } diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 827a9181d..a2869290e 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -48,7 +48,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA->getId()); $expected = array($packageA->getId()); @@ -64,7 +64,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -80,7 +80,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -96,7 +96,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); @@ -113,7 +113,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); @@ -136,7 +136,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($repo1); $this->repositorySet->addRepository($repo2); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($package1->getId(), $package2->getId(), $package3->getId(), $package4->getId()); $expected = array($package2->getId()); @@ -148,7 +148,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($repo2); $this->repositorySet->addRepository($repo1); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $expected = array($package4->getId()); $selected = $this->policy->selectPreferredPackages($pool, $literals); @@ -172,7 +172,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); $this->repositorySet->addRepository($this->repoLocked); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $packages = $pool->whatProvides('a', new Constraint('=', '2.1.9999999.9999999-dev')); $literals = array(); @@ -197,7 +197,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackages(array('A', 'B')); + $pool = $this->repositorySet->createPoolForPackages(array('A', 'B'), $this->repoLocked); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -216,7 +216,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackages(array('A', 'B')); + $pool = $this->repositorySet->createPoolForPackages(array('A', 'B'), $this->repoLocked); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -237,7 +237,7 @@ class DefaultPolicyTest extends TestCase $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer')); + $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer'), $this->repoLocked); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -253,7 +253,7 @@ class DefaultPolicyTest extends TestCase $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer')); + $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer'), $this->repoLocked); $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; @@ -270,7 +270,7 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); $this->repositorySet->addRepository($this->repo); - $pool = $this->repositorySet->createPoolForPackage('A'); + $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); From 7a4d3e6f25a50aaf950157ad8ae3329c6e76dafb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 15:03:11 +0100 Subject: [PATCH 176/321] Fix SolverTest --- tests/Composer/Test/DependencyResolver/SolverTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 4c733a4fb..4801188e4 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\DependencyResolver; use Composer\IO\NullIO; use Composer\Repository\ArrayRepository; +use Composer\Repository\LockArrayRepository; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; @@ -38,7 +39,7 @@ class SolverTest extends TestCase { $this->repoSet = new RepositorySet(array()); $this->repo = new ArrayRepository; - $this->repoLocked = new ArrayRepository; + $this->repoLocked = new LockArrayRepository; $this->request = new Request($this->repoLocked); $this->policy = new DefaultPolicy; From e6749d8717ab06fc289fa13aa3c465b7958b0d24 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 16:18:56 +0100 Subject: [PATCH 177/321] Add comment, fix 5.3 build --- src/Composer/DependencyResolver/PoolBuilder.php | 1 + src/Composer/Util/MetadataMinifier.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 05d029604..de297ff39 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -113,6 +113,7 @@ class PoolBuilder $loadNames = $newLoadNames; } + // filter packages according to all the require statements collected for each package 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 diff --git a/src/Composer/Util/MetadataMinifier.php b/src/Composer/Util/MetadataMinifier.php index 669dab54b..ba4cc0a93 100644 --- a/src/Composer/Util/MetadataMinifier.php +++ b/src/Composer/Util/MetadataMinifier.php @@ -52,7 +52,7 @@ class MetadataMinifier continue; } - $minifiedVersion = []; + $minifiedVersion = array(); // add any changes from the previous version foreach ($version as $key => $val) { From 257d2ce889bf0213900b320c7f4ca840237442ad Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 11:33:47 +0100 Subject: [PATCH 178/321] Avoid setting the update whitelist in require command on newly created files and when the lock file is disabled --- src/Composer/Command/RequireCommand.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index af16a3898..48851c28d 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -236,7 +236,6 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist(array_keys($requirements)) ->setWhitelistTransitiveDependencies($input->getOption('update-with-dependencies')) ->setWhitelistAllDependencies($input->getOption('update-with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) @@ -244,6 +243,12 @@ EOT ->setPreferLowest($input->getOption('prefer-lowest')) ; + // if no lock is present, or the file is brand new, we do not do a + // partial update as this is not supported by the Installer + if (!$this->newlyCreated && $composer->getConfig()->get('lock')) { + $install->setUpdateWhitelist(array_keys($requirements)); + } + $status = $install->run(); if ($status !== 0) { $this->revertComposerFile(false); From de189c1b803774d2a57efb4398d169fd8807b0ca Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 11:56:23 +0100 Subject: [PATCH 179/321] Fix deps=high build --- tests/Composer/Test/Downloader/GitDownloaderTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 3784b732a..1b3f1ebe2 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -601,6 +601,7 @@ composer https://github.com/old/url (push) $process->execute($expectedFirstGitUpdateCommand, Argument::cetera())->willReturn(1)->shouldBeCalled(); $process->execute($expectedSecondGitUpdateCommand, Argument::cetera())->willReturn(0)->shouldBeCalled(); $process->execute($this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), null, $this->winCompat($this->workingDir))->willReturn(0)->shouldBeCalled(); + $process->getErrorOutput()->willReturn(''); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock(null, new Config(), $process->reveal()); From e50f78043ab6a983217814cc89e6a6744205ac3f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 15:34:33 +0100 Subject: [PATCH 180/321] Try to load packages from lock file only and avoid loading other versions for pinned packages --- .../DependencyResolver/PoolBuilder.php | 41 +++++-------------- src/Composer/Installer.php | 3 +- .../Repository/InstalledArrayRepository.php | 2 +- src/Composer/Repository/RepositorySet.php | 7 ++++ .../Repository/RootPackageRepository.php | 24 +++++++++++ 5 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 src/Composer/Repository/RootPackageRepository.php diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index de297ff39..b150133d3 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -16,8 +16,6 @@ use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Package; use Composer\Package\PackageInterface; -use Composer\Repository\ComposerRepository; -use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\PlatformRepository; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MultiConstraint; @@ -54,13 +52,17 @@ class PoolBuilder // TODO do we really want the request here? kind of want a root requirements thingy instead $loadNames = array(); foreach ($request->getFixedPackages() as $package) { - // TODO can actually use very specific constraint - $loadNames[$package->getName()] = null; + $this->loadedNames[$package->getName()] = true; + unset($loadNames[$package->getName()]); + $loadNames += $this->loadPackage($request, $package); } foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': + if (isset($this->loadedNames[$job['packageName']])) { + continue 2; + } // TODO currently lock above is always NULL if we adjust that, this needs to merge constraints // TODO does it really make sense that we can have install requests for the same package that is actively locked with non-matching constraints? // also see the solver-problems.test test case @@ -71,27 +73,16 @@ class PoolBuilder } } - // packages from the locked repository only get loaded if they are explicitly fixed - foreach ($repositories as $key => $repository) { - if ($repository === $request->getLockedRepository()) { - foreach ($repository->getPackages() as $lockedPackage) { - foreach ($request->getFixedPackages() as $package) { - if ($package === $lockedPackage) { - $loadNames += $this->loadPackage($request, $package, $key); - } - } - } - } - } - while (!empty($loadNames)) { foreach ($loadNames as $name => $void) { $this->loadedNames[$name] = true; } $newLoadNames = array(); - foreach ($repositories as $key => $repository) { - if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface || $repository === $request->getLockedRepository()) { + foreach ($repositories as $repository) { + // these repos have their packages fixed if they need to be loaded so we + // never need to load anything else from them + if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) { continue; } @@ -103,9 +94,8 @@ class PoolBuilder unset($loadNames[$name]); } foreach ($result['packages'] as $package) { - if (call_user_func($this->isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { - $newLoadNames += $this->loadPackage($request, $package, $key); + $newLoadNames += $this->loadPackage($request, $package); } } } @@ -139,15 +129,6 @@ class PoolBuilder } } - foreach ($repositories as $key => $repository) { - if ($repository instanceof PlatformRepository || - $repository instanceof InstalledRepositoryInterface) { - foreach ($repository->getPackages() as $package) { - $this->loadPackage($request, $package, $key); - } - } - } - $pool->setPackages($this->packages); unset($this->aliasMap); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 646bb4fcf..9cba7f762 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -50,6 +50,7 @@ use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\InstalledArrayRepository; +use Composer\Repository\RootPackageRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryManager; @@ -735,7 +736,7 @@ class Installer $this->fixedRootPackage->setDevRequires(array()); $repositorySet = new RepositorySet($rootAliases, $this->package->getReferences(), $minimumStability, $stabilityFlags, $rootRequires); - $repositorySet->addRepository(new InstalledArrayRepository(array($this->fixedRootPackage))); + $repositorySet->addRepository(new RootPackageRepository(array($this->fixedRootPackage))); $repositorySet->addRepository($platformRepo); if ($this->additionalFixedRepository) { $repositorySet->addRepository($this->additionalFixedRepository); diff --git a/src/Composer/Repository/InstalledArrayRepository.php b/src/Composer/Repository/InstalledArrayRepository.php index c801d49ea..7ad05d0fa 100644 --- a/src/Composer/Repository/InstalledArrayRepository.php +++ b/src/Composer/Repository/InstalledArrayRepository.php @@ -15,7 +15,7 @@ namespace Composer\Repository; /** * Installed array repository. * - * This is used for serving the RootPackage inside an in-memory InstalledRepository + * This is used as an in-memory InstalledRepository mostly for testing purposes * * @author Jordi Boggiano */ diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 9b15a0115..5ffffb73d 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -20,6 +20,7 @@ use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\LockArrayRepository; +use Composer\Repository\InstalledRepositoryInterface; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Test\DependencyResolver\PoolTest; @@ -147,6 +148,12 @@ class RepositorySet { $poolBuilder = new PoolBuilder(array($this, 'isPackageAcceptable'), $this->rootRequires); + foreach ($this->repositories as $repo) { + if ($repo instanceof InstalledRepositoryInterface) { + throw new \LogicException('The pool can not accept packages from an installed repository'); + } + } + return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $this->rootReferences, $request); } diff --git a/src/Composer/Repository/RootPackageRepository.php b/src/Composer/Repository/RootPackageRepository.php new file mode 100644 index 000000000..8b5892717 --- /dev/null +++ b/src/Composer/Repository/RootPackageRepository.php @@ -0,0 +1,24 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +/** + * Root package repository. + * + * This is used for serving the RootPackage inside an in-memory InstalledRepository + * + * @author Jordi Boggiano + */ +class RootPackageRepository extends ArrayRepository +{ +} From 73bc137c3ca38b81e0b8ec0ee7437ce39aabd66e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 15 Jan 2020 16:21:23 +0100 Subject: [PATCH 181/321] Avoid nameConstraints from being collected for fixed packages --- src/Composer/DependencyResolver/PoolBuilder.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index b150133d3..cb6a34947 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -52,6 +52,7 @@ class PoolBuilder // TODO do we really want the request here? kind of want a root requirements thingy instead $loadNames = array(); foreach ($request->getFixedPackages() as $package) { + $this->nameConstraints[$package->getName()] = null; $this->loadedNames[$package->getName()] = true; unset($loadNames[$package->getName()]); $loadNames += $this->loadPackage($request, $package); From cc91e9164abd09f07f1e078aee54be66539c429d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 13:54:13 +0100 Subject: [PATCH 182/321] Stop unrolling the root aliases --- src/Composer/DependencyResolver/Request.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index d0502c1c5..62418738f 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -48,10 +48,6 @@ class Request */ public function fixPackage(PackageInterface $package, $lockable = true) { - if ($package instanceof RootAliasPackage) { - $package = $package->getAliasOf(); - } - $this->fixedPackages[spl_object_hash($package)] = $package; if (!$lockable) { From 6f44350c0108c4310fe5e85cfac70fce46a33aa0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 14:01:01 +0100 Subject: [PATCH 183/321] Remove problem reporting for locked package not being found, needs to be fixed in #7779 later --- tests/Composer/Test/Fixtures/installer/solver-problems.test | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 57d3ced92..7ca5b628f 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -61,12 +61,10 @@ Your requirements could not be resolved to an installable set of packages. Problem 2 - The requested package bogus/pkg could not be found in any version, there may be a typo in the package name. Problem 3 + - The requested package stable-requiree-excluded/pkg could not be found in any version, there may be a typo in the package name. + Problem 4 - Installation request for requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. - Problem 4 - - stable-requiree-excluded/pkg is locked to version 1.0.0 and an update of this package was not requested. - - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.0, 1.0.1]. - - Installation request for stable-requiree-excluded/pkg 1.0.1 -> satisfiable by stable-requiree-excluded/pkg[1.0.1]. Potential causes: - A typo in the package name From 572ef1add1e9343eccaf7876f9d94d18c0fd26f4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 14:25:43 +0100 Subject: [PATCH 184/321] Add comment for lockable --- src/Composer/DependencyResolver/Request.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 62418738f..fc4a21070 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -45,6 +45,8 @@ class Request /** * Mark an existing package as being installed and having to remain installed + * + * @param bool $lockable if set to false, the package will not be written to the lock file */ public function fixPackage(PackageInterface $package, $lockable = true) { From 56b2e1ae7ae2335f7292d740a3004901fae7293f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 14:26:52 +0100 Subject: [PATCH 185/321] Allow installing an aliased root package --- src/Composer/Installer.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9cba7f762..9f831ef8f 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -35,6 +35,7 @@ use Composer\Installer\NoopInstaller; use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; +use Composer\Package\RootAliasPackage; use Composer\Package\BasePackage; use Composer\Package\CompletePackage; use Composer\Package\Link; @@ -779,6 +780,9 @@ class Installer $request = new Request($lockedRepository); $request->fixPackage($rootPackage, false); + if ($rootPackage instanceof RootAliasPackage) { + $request->fixPackage($rootPackage->getAliasOf(), false); + } $fixedPackages = $platformRepo->getPackages(); if ($this->additionalFixedRepository) { From 8bb472a6089a14f58648fff8924c49a79c8545ec Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 14:51:26 +0100 Subject: [PATCH 186/321] No need to alias platform packages before the repository set as the pool builder already does it --- .../DependencyResolver/PoolBuilder.php | 1 - src/Composer/Installer.php | 21 ------------------- 2 files changed, 22 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index cb6a34947..1be0522a6 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -170,7 +170,6 @@ class PoolBuilder $aliasPackage = new AliasPackage($basePackage, $alias['alias_normalized'], $alias['alias']); $aliasPackage->setRootPackageAlias(true); - $package->getRepository()->addPackage($aliasPackage); // TODO do we need this? $this->packages[] = $aliasPackage; $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$index+1] = $aliasPackage; } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9f831ef8f..9ed7f2d8e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -697,8 +697,6 @@ class Installer */ private function createRepositorySet(PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) { - $this->aliasPlatformPackages($platformRepo, $rootAliases); - // TODO what's the point of rootConstraints at all, we generate the package pool taking them into account anyway? // TODO maybe we can drop the lockedRepository here // TODO if this gets called in doInstall, this->update is still true?! @@ -830,25 +828,6 @@ class Installer return $normalizedAliases; } - /** - * @param PlatformRepository $platformRepo - * @param array $aliases - */ - private function aliasPlatformPackages(PlatformRepository $platformRepo, $aliases) - { - // TODO should the repository set do this? - foreach ($aliases as $packageName => $versions) { - foreach ($versions as $version => $alias) { - $packages = $platformRepo->findPackages($packageName, $version); - foreach ($packages as $package) { - $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); - $aliasPackage->setRootPackageAlias(true); - $platformRepo->addPackage($aliasPackage); - } - } - } - } - /** * @param PackageInterface $package * @return bool From 98860b86199a4694f68e9ce2f19c8a38b89d0996 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 14:51:38 +0100 Subject: [PATCH 187/321] Fix show command when no package is found --- src/Composer/Command/ShowCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 998ea3802..073f9bdc0 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -560,7 +560,7 @@ EOT $matches[$index] = $package->getId(); } - $pool = $repositorySet->createPoolForPackage($package->getName()); + $pool = $repositorySet->createPoolForPackage($name); // select preferred package according to policy rules if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { From 304753ff693775f5897afaea71dd1cadf9aa2988 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 16 Jan 2020 07:58:50 +0100 Subject: [PATCH 188/321] Remove callback and pass stabilities all the way instead This allows optimizing the loading of ~dev files, and cleans up a few things --- .../DependencyResolver/PoolBuilder.php | 23 +++++----- src/Composer/Installer.php | 20 ++------- .../Package/Version/StabilityFilter.php | 43 +++++++++++++++++++ src/Composer/Repository/ArrayRepository.php | 5 ++- .../Repository/ComposerRepository.php | 31 ++++++------- .../Repository/CompositeRepository.php | 4 +- .../Repository/RepositoryInterface.php | 6 +-- src/Composer/Repository/RepositorySet.php | 35 ++++++--------- 8 files changed, 94 insertions(+), 73 deletions(-) create mode 100644 src/Composer/Package/Version/StabilityFilter.php diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 1be0522a6..0f44edf3b 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -25,10 +25,11 @@ use Composer\Semver\Constraint\MultiConstraint; */ class PoolBuilder { - private $isPackageAcceptableCallable; - private $rootRequires; + private $acceptableStabilities; + private $stabilityFlags; private $rootAliases; private $rootReferences; + private $rootRequires; private $aliasMap = array(); private $nameConstraints = array(); @@ -37,17 +38,18 @@ class PoolBuilder private $packages = array(); - public function __construct($isPackageAcceptableCallable, array $rootRequires = array()) + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, array $rootRequires = array()) { - $this->isPackageAcceptableCallable = $isPackageAcceptableCallable; + $this->acceptableStabilities = $acceptableStabilities; + $this->stabilityFlags = $stabilityFlags; + $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; $this->rootRequires = $rootRequires; } - public function buildPool(array $repositories, array $rootAliases, array $rootReferences, Request $request) + public function buildPool(array $repositories, Request $request) { $pool = new Pool(); - $this->rootAliases = $rootAliases; - $this->rootReferences = $rootReferences; // TODO do we really want the request here? kind of want a root requirements thingy instead $loadNames = array(); @@ -87,17 +89,14 @@ class PoolBuilder continue; } - // TODO should we really pass the callable into here? - $result = $repository->loadPackages($loadNames, $this->isPackageAcceptableCallable); + $result = $repository->loadPackages($loadNames, $this->acceptableStabilities, $this->stabilityFlags); foreach ($result['namesFound'] as $name) { // avoid loading the same package again from other repositories once it has been found unset($loadNames[$name]); } foreach ($result['packages'] as $package) { - if (call_user_func($this->isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { - $newLoadNames += $this->loadPackage($request, $package); - } + $newLoadNames += $this->loadPackage($request, $package); } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9ed7f2d8e..e9e8f34ea 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -364,19 +364,6 @@ class Installer $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); - if ($lockedRepository) { - // TODO do we really always need this? Maybe only to skip fix() in updateWhitelist case cause these packages get removed on full update automatically? - foreach ($lockedRepository->getPackages() as $lockedPackage) { - if (!$repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { - $constraint = new Constraint('=', $lockedPackage->getVersion()); - $constraint->setPrettyString('(stability not acceptable)'); - - // if we can get rid of this remove() here, we can generally get rid of remove support in the request - $request->remove($lockedPackage->getName(), $constraint); - } - } - } - $this->io->writeError('Updating dependencies'); $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); @@ -393,10 +380,11 @@ class Installer } // if the updateWhitelist is enabled, packages not in it are also fixed - // to the version specified in the lock - if ($this->updateWhitelist) { + // to the version specified in the lock, except if their stability is not + // acceptable anymore, to make sure that they get updated/downgraded to + // a working version + if ($this->updateWhitelist && $lockedRepository) { foreach ($lockedRepository->getPackages() as $lockedPackage) { - // TODO should this really be checking acceptability here? if (!$this->isUpdateable($lockedPackage) && $repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { // TODO add reason for fix? $request->fixPackage($lockedPackage); diff --git a/src/Composer/Package/Version/StabilityFilter.php b/src/Composer/Package/Version/StabilityFilter.php new file mode 100644 index 000000000..ed27af080 --- /dev/null +++ b/src/Composer/Package/Version/StabilityFilter.php @@ -0,0 +1,43 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Version; + +use Composer\Package\BasePackage; + +/** + * @author Jordi Boggiano + */ +class StabilityFilter +{ + /** + * Checks if any of the provided package names in the given stability match the configured acceptable stability and flags + * + * @return bool true if any package name is acceptable + */ + public static function isPackageAcceptable(array $acceptableStabilities, array $stabilityFlags, $names, $stability) + { + foreach ($names as $name) { + // allow if package matches the package-specific stability flag + if (isset($stabilityFlags[$name])) { + if (BasePackage::$stabilities[$stability] <= $stabilityFlags[$name]) { + return true; + } + } elseif (isset($acceptableStabilities[$stability])) { + // allow if package matches the global stability requirement and has no exception + return true; + } + } + + return false; + } +} diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index 42e9d50e1..6c7d0e65e 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -16,6 +16,7 @@ use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; +use Composer\Package\Version\StabilityFilter; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; @@ -44,7 +45,7 @@ class ArrayRepository extends BaseRepository /** * {@inheritDoc} */ - public function loadPackages(array $packageMap, $isPackageAcceptableCallable) + public function loadPackages(array $packageMap, array $acceptableStabilities, array $stabilityFlags) { $packages = $this->getPackages(); @@ -54,7 +55,7 @@ class ArrayRepository extends BaseRepository if (array_key_exists($package->getName(), $packageMap)) { if ( (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) - && call_user_func($isPackageAcceptableCallable, $package->getNames(), $package->getStability()) + && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, $package->getNames(), $package->getStability()) ) { $result[spl_object_hash($package)] = $package; if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 0ee04ff90..b23d5aced 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -16,6 +16,7 @@ use Composer\Package\Loader\ArrayLoader; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; +use Composer\Package\Version\StabilityFilter; use Composer\Json\JsonFile; use Composer\Cache; use Composer\Config; @@ -292,13 +293,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $names; } - public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable) + public function loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags) { // this call initializes loadRootServerFile which is needed for the rest below to work $hasProviders = $this->hasProviders(); if (!$hasProviders && !$this->hasPartialPackages() && !$this->lazyProvidersUrl) { - return parent::loadPackages($packageNameMap, $isPackageAcceptableCallable); + return parent::loadPackages($packageNameMap, $acceptableStabilities, $stabilityFlags); } $packages = array(); @@ -314,7 +315,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito continue; } - $candidates = $this->whatProvides($name, $isPackageAcceptableCallable); + $candidates = $this->whatProvides($name, $acceptableStabilities, $stabilityFlags); foreach ($candidates as $candidate) { if ($candidate->getName() !== $name) { throw new \LogicException('whatProvides should never return a package with a different name than the requested one'); @@ -350,7 +351,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito }, ARRAY_FILTER_USE_KEY); } - $result = $this->loadAsyncPackages($packageNameMap, $isPackageAcceptableCallable); + $result = $this->loadAsyncPackages($packageNameMap, $acceptableStabilities, $stabilityFlags); $packages = array_merge($packages, $result['packages']); $namesFound = array_merge($namesFound, $result['namesFound']); } @@ -444,7 +445,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito * @param callable $isPackageAcceptableCallable * @return array|mixed */ - private function whatProvides($name, $isPackageAcceptableCallable = null) + private function whatProvides($name, array $acceptableStabilities = null, array $stabilityFlags = null) { if (!$this->hasPartialPackages() || !isset($this->partialPackagesByName[$name])) { // skip platform packages, root package and composer-plugin-api @@ -533,7 +534,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $version['version_normalized'] = $this->versionParser->normalize($version['version']); } - if ($this->isVersionAcceptable($isPackageAcceptableCallable, null, $normalizedName, $version)) { + if ($this->isVersionAcceptable($acceptableStabilities, $stabilityFlags, null, $normalizedName, $version)) { $versionsToLoad[$version['uid']] = $version; } } @@ -590,7 +591,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito /** * @param array $packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only packages matching it will be loaded */ - private function loadAsyncPackages(array $packageNames, $isPackageAcceptableCallable = null) + private function loadAsyncPackages(array $packageNames, array $acceptableStabilities = null, array $stabilityFlags = null) { $this->loadRootServerFile(); @@ -603,11 +604,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito throw new \LogicException('loadAsyncPackages only supports v2 protocol composer repos with a metadata-url'); } - // load ~dev variants as well if present - // TODO ideally there should be a flag set from the repositoryset/poolbuilder to know which packages should have the dev packages loaded - // so we can optimize away some requests entirely + // load ~dev versions of the packages as well if needed foreach ($packageNames as $name => $constraint) { - $packageNames[$name.'~dev'] = $constraint; + if ($acceptableStabilities && $stabilityFlags && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), 'dev')) { + $packageNames[$name.'~dev'] = $constraint; + } } foreach ($packageNames as $name => $constraint) { @@ -629,7 +630,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified) - ->then(function ($response) use (&$packages, &$namesFound, $contents, $realName, $constraint, $repo, $isPackageAcceptableCallable) { + ->then(function ($response) use (&$packages, &$namesFound, $contents, $realName, $constraint, $repo, $acceptableStabilities, $stabilityFlags) { if (true === $response) { $response = $contents; } @@ -651,7 +652,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $version['version_normalized'] = $repo->versionParser->normalize($version['version']); } - if ($repo->isVersionAcceptable($isPackageAcceptableCallable, $constraint, $realName, $version)) { + if ($repo->isVersionAcceptable($acceptableStabilities, $stabilityFlags, $constraint, $realName, $version)) { $versionsToLoad[] = $version; } } @@ -681,7 +682,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito * @param string $name package name (must be lowercased already) * @private */ - public function isVersionAcceptable($isPackageAcceptableCallable, $constraint, $name, $versionData) + public function isVersionAcceptable(array $acceptableStabilities = null, array $stabilityFlags = null, $constraint = null, $name, $versionData) { $versions = array($versionData['version_normalized']); @@ -690,7 +691,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } foreach ($versions as $version) { - if ($isPackageAcceptableCallable && !call_user_func($isPackageAcceptableCallable, $name, VersionParser::parseStability($version))) { + if ($acceptableStabilities && $stabilityFlags && !StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), VersionParser::parseStability($version))) { continue; } diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index ddaa94694..8ead6693a 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -97,13 +97,13 @@ class CompositeRepository extends BaseRepository /** * {@inheritDoc} */ - public function loadPackages(array $packageMap, $isPackageAcceptableCallable) + public function loadPackages(array $packageMap, array $acceptableStabilities, array $stabilityFlags) { $packages = array(); $namesFound = array(); foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ - $result = $repository->findPackages($name, $constraint); + $result = $repository->loadPackages($packageMap, $acceptableStabilities, $stabilityFlags); $packages[] = $result['packages']; $namesFound[] = $result['namesFound']; } diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index f5e80c24c..8b1bb3cff 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -56,7 +56,6 @@ interface RepositoryInterface extends \Countable */ public function findPackages($name, $constraint = null); - // TODO this should really not be in this generic interface anymore /** * Returns list of registered packages. * @@ -68,10 +67,11 @@ interface RepositoryInterface extends \Countable * Returns list of registered packages with the supplied name * * @param ConstraintInterface[] $packageNameMap package names pointing to constraints - * @param $isPackageAcceptableCallable + * @param array $acceptableStabilities + * @param array $stabilityFlags * @return array [namesFound => string[], packages => PackageInterface[]] */ - public function loadPackages(array $packageNameMap, $isPackageAcceptableCallable); + public function loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags); /** * Searches the repository for packages containing the query diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 5ffffb73d..9ca6e98e3 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -22,7 +22,7 @@ use Composer\Repository\PlatformRepository; use Composer\Repository\LockArrayRepository; use Composer\Repository\InstalledRepositoryInterface; use Composer\Semver\Constraint\ConstraintInterface; -use Composer\Test\DependencyResolver\PoolTest; +use Composer\Package\Version\StabilityFilter; /** * @author Nils Adermann @@ -89,23 +89,6 @@ class RepositorySet } } - public function isPackageAcceptable($name, $stability) - { - foreach ((array) $name as $n) { - // allow if package matches the global stability requirement and has no exception - if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) { - return true; - } - - // allow if package matches the package-specific stability flag - if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) { - return true; - } - } - - return false; - } - /** * Find packages providing or matching a name and optionally meeting a constraint in all repositories * @@ -113,10 +96,11 @@ class RepositorySet * * @param string $name * @param ConstraintInterface|null $constraint - * @param bool $exactMatch + * @param bool $exactMatch if set to false, packages which replace/provide the given name might be returned as well even if they do not match the name exactly + * @param bool $ignoreStability if set to true, packages are returned even though their stability does not match the required stability * @return array */ - public function findPackages($name, ConstraintInterface $constraint = null, $exactMatch = true) + public function findPackages($name, ConstraintInterface $constraint = null, $exactMatch = true, $ignoreStability = false) { $packages = array(); foreach ($this->repositories as $repository) { @@ -131,7 +115,7 @@ class RepositorySet continue; } - if ($this->isPackageAcceptable($candidate->getNames(), $candidate->getStability())) { + if (!$ignoreStability && $this->isPackageAcceptable($candidate->getNames(), $candidate->getStability())) { $result[] = $candidate; } } @@ -139,6 +123,11 @@ class RepositorySet return $candidates; } + public function isPackageAcceptable($names, $stability) + { + return StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $names, $stability); + } + /** * Create a pool for dependency resolution from the packages in this repository set. * @@ -146,7 +135,7 @@ class RepositorySet */ public function createPool(Request $request) { - $poolBuilder = new PoolBuilder(array($this, 'isPackageAcceptable'), $this->rootRequires); + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $this->rootRequires); foreach ($this->repositories as $repo) { if ($repo instanceof InstalledRepositoryInterface) { @@ -154,7 +143,7 @@ class RepositorySet } } - return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $this->rootReferences, $request); + return $this->pool = $poolBuilder->buildPool($this->repositories, $request); } // TODO unify this with above in some simpler version without "request"? From e162cc6f0a031c7405bced971f6978801ecc4c23 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 10:08:21 +0100 Subject: [PATCH 189/321] Remove "remove" from request --- src/Composer/DependencyResolver/Problem.php | 4 ---- src/Composer/DependencyResolver/Request.php | 5 ----- 2 files changed, 9 deletions(-) diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 8c73bd91d..54815995f 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -224,10 +224,6 @@ class Problem } return 'Installation request for '.$packageName.$this->constraintToText($constraint).' -> satisfiable by '.$this->getPackageList($packages).'.'; - case 'update': - return 'Update request for '.$packageName.$this->constraintToText($constraint).'.'; - case 'remove': - return 'Removal request for '.$packageName.$this->constraintToText($constraint).''; } if (isset($constraint)) { diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index fc4a21070..851407591 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -38,11 +38,6 @@ class Request $this->addJob($packageName, 'install', $constraint); } - public function remove($packageName, ConstraintInterface $constraint = null) - { - $this->addJob($packageName, 'remove', $constraint); - } - /** * Mark an existing package as being installed and having to remain installed * From b5e34ca7674a7edb3c88e0fa1c2e1d71bf0218fc Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 10:27:12 +0100 Subject: [PATCH 190/321] Fix remove tests --- .../Test/DependencyResolver/RequestTest.php | 4 +--- .../Test/DependencyResolver/SolverTest.php | 23 ------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index 405f3b0c8..7534f9a5c 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -18,7 +18,7 @@ use Composer\Test\TestCase; class RequestTest extends TestCase { - public function testRequestInstallAndRemove() + public function testRequestInstall() { $repo = new ArrayRepository; $foo = $this->getPackage('foo', '1'); @@ -31,12 +31,10 @@ class RequestTest extends TestCase $request = new Request(); $request->install('foo'); - $request->remove('foobar'); $this->assertEquals( array( array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null), - array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null), ), $request->getJobs() ); diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 4801188e4..01a518f27 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -196,28 +196,6 @@ class SolverTest extends TestCase $this->checkSolverResult(array()); } - public function testSolverRemoveSingle() - { - $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->reposComplete(); - - $this->request->remove('A'); - - $this->checkSolverResult(array( - array('job' => 'remove', 'package' => $packageA), - )); - } - - public function testSolverRemoveUninstalled() - { - $this->repo->addPackage($this->getPackage('A', '1.0')); - $this->reposComplete(); - - $this->request->remove('A'); - - $this->checkSolverResult(array()); - } - public function testSolverUpdateDoesOnlyUpdate() { $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); @@ -367,7 +345,6 @@ class SolverTest extends TestCase $this->request->install('A'); $this->request->install('C'); - $this->request->remove('D'); $this->checkSolverResult(array( array('job' => 'remove', 'package' => $packageD), From 6dc576738a3a0eb68281a3bb4117f4d25d3b5303 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 15:15:46 +0100 Subject: [PATCH 191/321] Avoid partial updates from applying changes to packages which are not locked with an acceptable stability --- src/Composer/DependencyResolver/PoolBuilder.php | 9 ++++++++- src/Composer/DependencyResolver/RuleSetGenerator.php | 3 ++- src/Composer/Installer.php | 6 ++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 0f44edf3b..7eaae9423 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -16,7 +16,9 @@ use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Package; use Composer\Package\PackageInterface; +use Composer\Package\Version\StabilityFilter; use Composer\Repository\PlatformRepository; +use Composer\Repository\RootPackageRepository; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MultiConstraint; @@ -57,7 +59,12 @@ class PoolBuilder $this->nameConstraints[$package->getName()] = null; $this->loadedNames[$package->getName()] = true; unset($loadNames[$package->getName()]); - $loadNames += $this->loadPackage($request, $package); + if ( + $package->getRepository() instanceof RootPackageRepository + || StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability()) + ) { + $loadNames += $this->loadPackage($request, $package); + } } foreach ($request->getJobs() as $job) { diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 442be135c..bdf187470 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -290,8 +290,9 @@ class RuleSetGenerator $unlockableMap = $request->getUnlockableMap(); foreach ($request->getFixedPackages() as $package) { + // fixed package was not added to the pool which must mean it did not pass the stability requirements if ($package->id == -1) { - throw new \RuntimeException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool."); + continue; } $this->addRulesForPackage($package, $ignorePlatformReqs); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index e9e8f34ea..9c6c5a719 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -380,12 +380,10 @@ class Installer } // if the updateWhitelist is enabled, packages not in it are also fixed - // to the version specified in the lock, except if their stability is not - // acceptable anymore, to make sure that they get updated/downgraded to - // a working version + // to the version specified in the lock if ($this->updateWhitelist && $lockedRepository) { foreach ($lockedRepository->getPackages() as $lockedPackage) { - if (!$this->isUpdateable($lockedPackage) && $repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { + if (!$this->isUpdateable($lockedPackage)) { // TODO add reason for fix? $request->fixPackage($lockedPackage); } From ebe910c3a55b2767549350de114f3c63f243d5c3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 15:16:01 +0100 Subject: [PATCH 192/321] Tweak test to follow changes --- ...e-downgrades-non-whitelisted-unstable.test | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index d87df634c..9735d0a2c 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -48,25 +48,23 @@ Partial update from lock file should apply lock file and downgrade unstable pack ] --RUN-- update c/uptodate ---EXPECT-LOCK-- -{ - "packages": [ - { "name": "a/old", "version": "1.0.0", "type": "library" }, - { "name": "b/unstable", "version": "1.0.0", "type": "library" }, - { "name": "c/uptodate", "version": "1.0.0", "type": "library" }, - { "name": "d/removed", "version": "1.0.0", "type": "library" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} --EXPECT-- -Updating a/old (0.9.0 => 1.0.0) -Updating b/unstable (1.1.0-alpha => 1.0.0) -Updating c/uptodate (2.0.0 => 1.0.0) -Installing d/removed (1.0.0) + +--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 + - The requested package b/unstable could not be found in any version, there may be a typo in the package name. + +Potential causes: + - A typo in the package name + - The package is not available in a stable-enough version according to your minimum-stability setting + see for more details. + - It's a private package and you forgot to add a custom repository to find it + +Read for further common problems. From 7cc8a4aed8f52f5971f16782ee21c20d783c6ede Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 15:29:30 +0100 Subject: [PATCH 193/321] Avoid checking stability on platform packages too --- src/Composer/DependencyResolver/PoolBuilder.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 7eaae9423..9d004758c 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -61,6 +61,7 @@ class PoolBuilder unset($loadNames[$package->getName()]); if ( $package->getRepository() instanceof RootPackageRepository + || $package->getRepository() instanceof PlatformRepository || StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability()) ) { $loadNames += $this->loadPackage($request, $package); From c6a3f48eaf0dd5824b2e22e7267bfc0e730527a2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 15:35:37 +0100 Subject: [PATCH 194/321] Remove some more remove request handling --- src/Composer/DependencyResolver/RuleSetGenerator.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index bdf187470..7bed3bb0b 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -325,15 +325,6 @@ class RuleSetGenerator $this->addRule(RuleSet::TYPE_JOB, $rule); } break; - case 'remove': - // remove all packages with this name including uninstalled - // ones to make sure none of them are picked as replacements - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); - foreach ($packages as $package) { - $rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE, $job); - $this->addRule(RuleSet::TYPE_JOB, $rule); - } - break; } } } From 1d3119047294476244e872552671a12a41994aed Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 15:48:31 +0100 Subject: [PATCH 195/321] Keep track of unacceptable fixed packages for later to use in error reporting and make sure the pool state is consistent --- src/Composer/DependencyResolver/Pool.php | 17 ++++++++++++----- src/Composer/DependencyResolver/PoolBuilder.php | 15 +++++++++------ .../DependencyResolver/RuleSetGenerator.php | 9 +++++++-- .../Test/DependencyResolver/PoolTest.php | 13 +++++-------- .../Test/DependencyResolver/RuleSetTest.php | 3 +-- .../Test/DependencyResolver/RuleTest.php | 3 +-- 6 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index ffb70c300..69a5b2cc2 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -38,24 +38,26 @@ class Pool implements \Countable protected $packageByExactName = array(); protected $versionParser; protected $providerCache = array(); + protected $unacceptableFixedPackages; - public function __construct() + public function __construct(array $packages = array(), array $unacceptableFixedPackages = array()) { $this->versionParser = new VersionParser; + $this->setPackages($packages); + $this->unacceptableFixedPackages = $unacceptableFixedPackages; } - public function setPackages(array $packages) + private function setPackages(array $packages) { $id = 1; - foreach ($packages as $i => $package) { + foreach ($packages as $package) { $this->packages[] = $package; $package->id = $id++; - $names = $package->getNames(); $this->packageByExactName[$package->getName()][$package->id] = $package; - foreach ($names as $provided) { + foreach ($package->getNames() as $provided) { $this->packageByName[$provided][] = $package; } } @@ -227,4 +229,9 @@ class Pool implements \Countable return self::MATCH_NONE; } + + public function isUnacceptableFixedPackage(PackageInterface $package) + { + return in_array($package, $this->unacceptableFixedPackages, true); + } } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 9d004758c..9cc6e18c1 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -35,10 +35,9 @@ class PoolBuilder private $aliasMap = array(); private $nameConstraints = array(); - private $loadedNames = array(); - private $packages = array(); + private $unacceptableFixedPackages = array(); public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, array $rootRequires = array()) { @@ -65,6 +64,8 @@ class PoolBuilder || StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability()) ) { $loadNames += $this->loadPackage($request, $package); + } else { + $this->unacceptableFixedPackages[] = $package; } } @@ -137,11 +138,13 @@ class PoolBuilder } } - $pool->setPackages($this->packages); + $pool = new Pool($this->packages, $this->unacceptableFixedPackages); - unset($this->aliasMap); - unset($this->loadedNames); - unset($this->nameConstraints); + $this->aliasMap = array(); + $this->nameConstraints = array(); + $this->loadedNames = array(); + $this->packages = array(); + $this->unacceptableFixedPackages = array(); return $pool; } diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 7bed3bb0b..01b75e712 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -290,9 +290,14 @@ class RuleSetGenerator $unlockableMap = $request->getUnlockableMap(); foreach ($request->getFixedPackages() as $package) { - // fixed package was not added to the pool which must mean it did not pass the stability requirements if ($package->id == -1) { - continue; + // fixed package was not added to the pool as it did not pass the stability requirements, this is fine + if ($this->pool->isUnacceptableFixedPackage($package)) { + continue; + } + + // otherwise, looks like a bug + throw new \LogicException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool."); } $this->addRulesForPackage($package, $ignorePlatformReqs); diff --git a/tests/Composer/Test/DependencyResolver/PoolTest.php b/tests/Composer/Test/DependencyResolver/PoolTest.php index dba110691..ceeb928ba 100644 --- a/tests/Composer/Test/DependencyResolver/PoolTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolTest.php @@ -21,10 +21,9 @@ class PoolTest extends TestCase { public function testPool() { - $pool = $this->createPool(); $package = $this->getPackage('foo', '1'); - $pool->setPackages(array($package)); + $pool = $this->createPool(array($package)); $this->assertEquals(array($package), $pool->whatProvides('foo')); $this->assertEquals(array($package), $pool->whatProvides('foo')); @@ -32,12 +31,11 @@ class PoolTest extends TestCase public function testWhatProvidesPackageWithConstraint() { - $pool = $this->createPool(); $firstPackage = $this->getPackage('foo', '1'); $secondPackage = $this->getPackage('foo', '2'); - $pool->setPackages(array( + $pool = $this->createPool(array( $firstPackage, $secondPackage, )); @@ -48,10 +46,9 @@ class PoolTest extends TestCase public function testPackageById() { - $pool = $this->createPool(); $package = $this->getPackage('foo', '1'); - $pool->setPackages(array($package)); + $pool = $this->createPool(array($package)); $this->assertSame($package, $pool->packageById(1)); } @@ -63,8 +60,8 @@ class PoolTest extends TestCase $this->assertEquals(array(), $pool->whatProvides('foo')); } - protected function createPool() + protected function createPool(array $packages = array()) { - return new Pool(); + return new Pool($packages); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 05780e526..290b3af2f 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -139,8 +139,7 @@ class RuleSetTest extends TestCase public function testPrettyString() { - $pool = new Pool(); - $pool->setPackages(array( + $pool = new Pool(array( $p = $this->getPackage('foo', '2.1'), )); diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 3a0e2e5ca..8941f430e 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -93,8 +93,7 @@ class RuleTest extends TestCase public function testPrettyString() { - $pool = new Pool(); - $pool->setPackages(array( + $pool = new Pool(array( $p1 = $this->getPackage('foo', '2.1'), $p2 = $this->getPackage('baz', '1.1'), )); From 921a97457dffbb2e969b54abb13deaa64238c95b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 19 Jan 2020 19:46:16 +0100 Subject: [PATCH 196/321] Add PoolBuilder test harness for writing .test files --- ...fixed-packages-do-not-load-from-repos.test | 23 +++ ...minimum-stability-and-filter-packages.test | 44 ++++ .../DependencyResolver/PoolBuilderTest.php | 194 ++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/fixed-packages-do-not-load-from-repos.test create mode 100644 tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/stability-flags-take-over-minimum-stability-and-filter-packages.test create mode 100644 tests/Composer/Test/DependencyResolver/PoolBuilderTest.php diff --git a/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/fixed-packages-do-not-load-from-repos.test b/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/fixed-packages-do-not-load-from-repos.test new file mode 100644 index 000000000..f1dbd69fe --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/fixed-packages-do-not-load-from-repos.test @@ -0,0 +1,23 @@ +--TEST-- +Fixed packages do not get loaded from the repos + +--REQUEST-- +{ + "some/pkg": "*" +} + +--FIXED-- +[ + {"name": "some/pkg", "version": "1.0.3", "id": 1} +] + +--PACKAGES-- +[ + {"name": "some/pkg", "version": "1.0.0"}, + {"name": "some/pkg", "version": "1.1.0"} +] + +--EXPECT-- +[ + 1 +] diff --git a/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/stability-flags-take-over-minimum-stability-and-filter-packages.test b/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/stability-flags-take-over-minimum-stability-and-filter-packages.test new file mode 100644 index 000000000..5a6ca8f2f --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/stability-flags-take-over-minimum-stability-and-filter-packages.test @@ -0,0 +1,44 @@ +--TEST-- +Stability flags apply + +--ROOT-- +{ + "stability-flags": { + "flagged/pkg": "alpha" + }, + "minimum-stability": "RC", + "aliases": [ + { + "package": "default/pkg", + "version": "1.0.0-RC", + "alias": "1.2.0" + } + ] +} + +--REQUEST-- +{ + "flagged/pkg": "*", + "default/pkg": "*" +} + +--PACKAGES-- +[ + {"name": "flagged/pkg", "version": "1.0.0", "id": 1}, + {"name": "flagged/pkg", "version": "1.0.0-beta", "id": 2}, + {"name": "flagged/pkg", "version": "1.0.0-dev", "id": 3}, + {"name": "flagged/pkg", "version": "1.0.0-RC", "id": 4}, + {"name": "default/pkg", "version": "1.0.0", "id": 5}, + {"name": "default/pkg", "version": "1.0.0-RC", "id": 6}, + {"name": "default/pkg", "version": "1.0.0-alpha", "id": 7} +] + +--EXPECT-- +[ + 1, + 2, + 4, + 5, + 6, + "default/pkg-1.2.0.0 alias of 6" +] diff --git a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php new file mode 100644 index 000000000..54a5e8e3c --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php @@ -0,0 +1,194 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\DependencyResolver; + +use Composer\IO\NullIO; +use Composer\Repository\ArrayRepository; +use Composer\Repository\LockArrayRepository; +use Composer\DependencyResolver\DefaultPolicy; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\PoolBuilder; +use Composer\DependencyResolver\Request; +use Composer\DependencyResolver\Solver; +use Composer\DependencyResolver\SolverProblemsException; +use Composer\Package\BasePackage; +use Composer\Package\AliasPackage; +use Composer\Json\JsonFile; +use Composer\Package\Loader\ArrayLoader; +use Composer\Package\Version\VersionParser; +use Composer\Repository\InstalledArrayRepository; +use Composer\Repository\RepositorySet; +use Composer\Test\TestCase; +use Composer\Semver\Constraint\MultiConstraint; + +class PoolBuilderTest extends TestCase +{ + /** + * @dataProvider getIntegrationTests + */ + public function testPoolBuilder($file, $message, $expect, $root, $requestData, $packages, $fixed) + { + $rootAliases = !empty($root['aliases']) ? $root['aliases'] : array(); + $minimumStability = !empty($root['minimum-stability']) ? $root['minimum-stability'] : 'stable'; + $stabilityFlags = !empty($root['stability-flags']) ? $root['stability-flags'] : array(); + $stabilityFlags = array_map(function ($stability) { + return BasePackage::$stabilities[$stability]; + }, $stabilityFlags); + + $parser = new VersionParser(); + $normalizedAliases = array(); + foreach ($rootAliases as $alias) { + $normalizedAliases[$alias['package']][$parser->normalize($alias['version'])] = array( + 'alias' => $alias['alias'], + 'alias_normalized' => $parser->normalize($alias['alias']), + ); + } + + $loader = new ArrayLoader(); + $packageIds = array(); + $loadPackage = function ($data) use ($loader, &$packageIds) { + if (!empty($data['id'])) { + $id = $data['id']; + unset($data['id']); + } + + $pkg = $loader->load($data); + + if (!empty($id)) { + if (!empty($packageIds[$id])) { + throw new \LogicException('Duplicate package id '.$id.' defined'); + } + $packageIds[$id] = $pkg; + } + + return $pkg; + }; + + $repositorySet = new RepositorySet($normalizedAliases, array(), $minimumStability, $stabilityFlags); + $repositorySet->addRepository($repo = new ArrayRepository()); + foreach ($packages as $package) { + $repo->addPackage($loadPackage($package)); + } + + $request = new Request(); + foreach ($requestData as $package => $constraint) { + $request->install($package, $parser->parseConstraints($constraint)); + } + + foreach ($fixed as $fixedPackage) { + $request->fixPackage($loadPackage($fixedPackage)); + } + + $pool = $repositorySet->createPool($request); + for ($i = 1, $count = count($pool); $i <= $count; $i++) { + $result[] = $pool->packageById($i); + } + + $result = array_map(function ($package) use ($packageIds) { + if ($id = array_search($package, $packageIds, true)) { + return $id; + } + + if ($package instanceof AliasPackage && $id = array_search($package->getAliasOf(), $packageIds, true)) { + return (string) $package->getName().'-'.$package->getVersion() .' alias of '.$id; + } + + return (string) $package; + }, $result); + + $this->assertSame($expect, $result); + } + + public function getIntegrationTests() + { + $fixturesDir = realpath(__DIR__.'/Fixtures/poolbuilder/'); + $tests = array(); + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if (!preg_match('/\.test$/', $file)) { + continue; + } + + try { + $testData = $this->readTestFile($file, $fixturesDir); + + $message = $testData['TEST']; + + $request = JsonFile::parseJson($testData['REQUEST']); + $root = !empty($testData['ROOT']) ? JsonFile::parseJson($testData['ROOT']) : array(); + + $packages = JsonFile::parseJson($testData['PACKAGES']); + $fixed = array(); + if (!empty($testData['FIXED'])) { + $fixed = JsonFile::parseJson($testData['FIXED']); + } + $expect = JsonFile::parseJson($testData['EXPECT']); + } catch (\Exception $e) { + die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); + } + + $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $expect, $root, $request, $packages, $fixed); + } + + return $tests; + } + + protected function readTestFile(\SplFileInfo $file, $fixturesDir) + { + $tokens = preg_split('#(?:^|\n*)--([A-Z-]+)--\n#', file_get_contents($file->getRealPath()), null, PREG_SPLIT_DELIM_CAPTURE); + + $sectionInfo = array( + 'TEST' => true, + 'ROOT' => false, + 'REQUEST' => true, + 'FIXED' => false, + 'PACKAGES' => true, + 'EXPECT' => true, + ); + + $section = null; + foreach ($tokens as $i => $token) { + if (null === $section && empty($token)) { + continue; // skip leading blank + } + + if (null === $section) { + if (!isset($sectionInfo[$token])) { + throw new \RuntimeException(sprintf( + 'The test file "%s" must not contain a section named "%s".', + str_replace($fixturesDir.'/', '', $file), + $token + )); + } + $section = $token; + continue; + } + + $sectionData = $token; + + $data[$section] = $sectionData; + $section = $sectionData = null; + } + + foreach ($sectionInfo as $section => $required) { + if ($required && !isset($data[$section])) { + throw new \RuntimeException(sprintf( + 'The test file "%s" must have a section named "%s".', + str_replace($fixturesDir.'/', '', $file), + $section + )); + } + } + + return $data; + } +} From 5bdc0fc9c5cc107d889dbbce30e2e4483673f2c5 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sun, 19 Jan 2020 23:11:36 +0100 Subject: [PATCH 197/321] Request jobs replaced by root require / fixed package The only type of request job remaining was "install" which is really a root requirement. The only other kind of input for the solver is now a set of fixed packages. Rules have been updated to account for only two kinds of former job reason: FIXED or ROOT_REQUIRE. The job property has always been redundant and has been removed, since reasonData suffices. Problem reasons are always rules, so the unnecessary wrapping in an array has been removed. We now only ever generate a single rule per root require or fixed package, so there is no need for the solver to special handle disabling "jobs" anymore, the rule can just be disabled as usual. For consistency special handling of rules for jobs in problems has been integrated into the rule class like all other rule reasons. As part of this change the error message for root requirements has been improved a bit to make it clearer where the package installation request came from. The word job has also been removed from operations, which are called operations, not jobs. --- .../DependencyResolver/GenericRule.php | 5 +- .../DependencyResolver/MultiConflictRule.php | 5 +- .../Operation/InstallOperation.php | 4 +- .../Operation/MarkAliasInstalledOperation.php | 4 +- .../MarkAliasUninstalledOperation.php | 4 +- .../Operation/OperationInterface.php | 4 +- .../Operation/UninstallOperation.php | 4 +- .../Operation/UpdateOperation.php | 4 +- .../DependencyResolver/PoolBuilder.php | 22 ++--- src/Composer/DependencyResolver/Problem.php | 81 ++++----------- src/Composer/DependencyResolver/Request.php | 22 ++--- src/Composer/DependencyResolver/Rule.php | 53 ++++++---- .../DependencyResolver/Rule2Literals.php | 5 +- src/Composer/DependencyResolver/RuleSet.php | 4 +- .../DependencyResolver/RuleSetGenerator.php | 59 ++++------- src/Composer/DependencyResolver/Solver.php | 62 ++++-------- .../SolverProblemsException.php | 4 +- .../DependencyResolver/Transaction.php | 2 +- src/Composer/Installer.php | 13 ++- .../Installer/InstallationManager.php | 38 +++---- src/Composer/Package/Link.php | 2 +- src/Composer/Repository/RepositorySet.php | 2 +- .../DependencyResolver/PoolBuilderTest.php | 2 +- .../Test/DependencyResolver/RequestTest.php | 12 +-- .../RuleSetIteratorTest.php | 14 +-- .../Test/DependencyResolver/RuleSetTest.php | 62 ++++++------ .../Test/DependencyResolver/RuleTest.php | 33 ++++--- .../Test/DependencyResolver/SolverTest.php | 98 +++++++++---------- .../DependencyResolver/TransactionTest.php | 4 +- .../installer/broken-deps-do-not-replace.test | 2 +- .../installer/github-issues-4319.test | 2 +- .../Fixtures/installer/solver-problems.test | 2 +- .../Test/Mock/InstallationManagerMock.php | 2 +- 33 files changed, 271 insertions(+), 365 deletions(-) diff --git a/src/Composer/DependencyResolver/GenericRule.php b/src/Composer/DependencyResolver/GenericRule.php index eb753067e..a07883872 100644 --- a/src/Composer/DependencyResolver/GenericRule.php +++ b/src/Composer/DependencyResolver/GenericRule.php @@ -26,11 +26,10 @@ class GenericRule extends Rule * @param array $literals * @param int|null $reason A RULE_* constant describing the reason for generating this rule * @param Link|PackageInterface|int|null $reasonData - * @param array $job The job this rule was created from */ - public function __construct(array $literals, $reason, $reasonData, $job = null) + public function __construct(array $literals, $reason, $reasonData) { - parent::__construct($reason, $reasonData, $job); + parent::__construct($reason, $reasonData); // sort all packages ascending by id sort($literals); diff --git a/src/Composer/DependencyResolver/MultiConflictRule.php b/src/Composer/DependencyResolver/MultiConflictRule.php index f2c27c406..8de77a41b 100644 --- a/src/Composer/DependencyResolver/MultiConflictRule.php +++ b/src/Composer/DependencyResolver/MultiConflictRule.php @@ -28,11 +28,10 @@ class MultiConflictRule extends Rule * @param array $literals * @param int $reason A RULE_* constant describing the reason for generating this rule * @param Link|PackageInterface $reasonData - * @param array $job The job this rule was created from */ - public function __construct(array $literals, $reason, $reasonData, $job = null) + public function __construct(array $literals, $reason, $reasonData) { - parent::__construct($reason, $reasonData, $job); + parent::__construct($reason, $reasonData); if (count($literals) < 3) { throw new \RuntimeException("multi conflict rule requires at least 3 literals"); diff --git a/src/Composer/DependencyResolver/Operation/InstallOperation.php b/src/Composer/DependencyResolver/Operation/InstallOperation.php index c451446b5..b719f49f9 100644 --- a/src/Composer/DependencyResolver/Operation/InstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/InstallOperation.php @@ -47,11 +47,11 @@ class InstallOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'install'; } diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php index 7cecf565c..e028b3079 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php @@ -48,11 +48,11 @@ class MarkAliasInstalledOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'markAliasInstalled'; } diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php index c84acf5af..0468f2daf 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php @@ -48,11 +48,11 @@ class MarkAliasUninstalledOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'markAliasUninstalled'; } diff --git a/src/Composer/DependencyResolver/Operation/OperationInterface.php b/src/Composer/DependencyResolver/Operation/OperationInterface.php index 261a746af..be5e8f7af 100644 --- a/src/Composer/DependencyResolver/Operation/OperationInterface.php +++ b/src/Composer/DependencyResolver/Operation/OperationInterface.php @@ -20,11 +20,11 @@ namespace Composer\DependencyResolver\Operation; interface OperationInterface { /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType(); + public function getOperationType(); /** * Returns operation reason. diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index 0f990e57b..704635b2a 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -47,11 +47,11 @@ class UninstallOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'uninstall'; } diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index d846f3da1..4764acb7c 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -60,11 +60,11 @@ class UpdateOperation extends SolverOperation } /** - * Returns job type. + * Returns operation type. * * @return string */ - public function getJobType() + public function getOperationType() { return 'update'; } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 9cc6e18c1..bcfc67fea 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -69,20 +69,16 @@ class PoolBuilder } } - foreach ($request->getJobs() as $job) { - switch ($job['cmd']) { - case 'install': - if (isset($this->loadedNames[$job['packageName']])) { - continue 2; - } - // TODO currently lock above is always NULL if we adjust that, this needs to merge constraints - // TODO does it really make sense that we can have install requests for the same package that is actively locked with non-matching constraints? - // also see the solver-problems.test test case - $constraint = array_key_exists($job['packageName'], $loadNames) ? null : $job['constraint']; - $loadNames[$job['packageName']] = $constraint; - $this->nameConstraints[$job['packageName']] = $constraint ? new MultiConstraint(array($constraint), false) : null; - break; + foreach ($request->getRequires() as $packageName => $constraint) { + if (isset($this->loadedNames[$packageName])) { + continue; } + // TODO currently lock above is always NULL if we adjust that, this needs to merge constraints + // TODO does it really make sense that we can have install requests for the same package that is actively locked with non-matching constraints? + // also see the solver-problems.test test case + $constraint = array_key_exists($packageName, $loadNames) ? null : $constraint; + $loadNames[$packageName] = $constraint; + $this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null; } while (!empty($loadNames)) { diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 54815995f..130681217 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -28,7 +28,7 @@ class Problem protected $reasonSeen; /** - * A set of reasons for the problem, each is a rule or a job and a rule + * A set of reasons for the problem, each is a rule or a root require and a rule * @var array */ protected $reasons = array(); @@ -49,10 +49,7 @@ class Problem */ public function addRule(Rule $rule) { - $this->addReason(spl_object_hash($rule), array( - 'rule' => $rule, - 'job' => $rule->getJob(), - )); + $this->addReason(spl_object_hash($rule), $rule); } /** @@ -73,16 +70,21 @@ class Problem */ public function getPrettyString(array $installedMap = array(), array $learnedPool = array()) { + // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections? $reasons = call_user_func_array('array_merge', array_reverse($this->reasons)); if (count($reasons) === 1) { reset($reasons); - $reason = current($reasons); + $rule = current($reasons); - $job = $reason['job']; + if (!in_array($rule->getReason(), array(Rule::RULE_ROOT_REQUIRE, Rule::RULE_FIXED), true)) { + throw new \LogicException("Single reason problems must contain a request rule."); + } - $packageName = $job['packageName']; - $constraint = $job['constraint']; + $request = $rule->getReasonData(); + + $packageName = $request['packageName']; + $constraint = $request['constraint']; if (isset($constraint)) { $packages = $this->pool->whatProvides($packageName, $constraint); @@ -90,8 +92,7 @@ class Problem $packages = array(); } - if ($job && ($job['cmd'] === 'install' || $job['cmd'] === 'fix') && empty($packages)) { - + if ($request && empty($packages)) { // handle php/hhvm if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { $version = phpversion(); @@ -164,17 +165,8 @@ class Problem $messages = array(); - foreach ($reasons as $reason) { - $rule = $reason['rule']; - $job = $reason['job']; - - if ($job) { - $messages[] = $this->jobToText($job); - } elseif ($rule) { - if ($rule instanceof Rule) { - $messages[] = $rule->getPrettyString($this->pool, $installedMap, $learnedPool); - } - } + foreach ($reasons as $rule) { + $messages[] = $rule->getPrettyString($this->pool, $installedMap, $learnedPool); } return "\n - ".implode("\n - ", $messages); @@ -184,10 +176,13 @@ class Problem * Store a reason descriptor but ignore duplicates * * @param string $id A canonical identifier for the reason - * @param string|array $reason The reason descriptor + * @param Rule $reason The reason descriptor */ - protected function addReason($id, $reason) + protected function addReason($id, Rule $reason) { + // TODO: if a rule is part of a problem description in two sections, isn't this going to remove a message + // that is important to understand the issue? + if (!isset($this->reasonSeen[$id])) { $this->reasonSeen[$id] = true; $this->reasons[$this->section][] = $reason; @@ -199,42 +194,6 @@ class Problem $this->section++; } - /** - * Turns a job into a human readable description - * - * @param array $job - * @return string - */ - protected function jobToText($job) - { - $packageName = $job['packageName']; - $constraint = $job['constraint']; - switch ($job['cmd']) { - case 'fix': - $package = $job['package']; - if ($job['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'; - case 'install': - $packages = $this->pool->whatProvides($packageName, $constraint); - if (!$packages) { - return 'No package found to satisfy install request for '.$packageName.$this->constraintToText($constraint); - } - - return 'Installation request for '.$packageName.$this->constraintToText($constraint).' -> satisfiable by '.$this->getPackageList($packages).'.'; - } - - if (isset($constraint)) { - $packages = $this->pool->whatProvides($packageName, $constraint); - } else { - $packages = $this->pool->whatProvides($job['packageName'], null); - } - - return 'Job(cmd='.$job['cmd'].', target='.$packageName.', packages=['.$this->getPackageList($packages).'])'; - } - protected function getPackageList($packages) { $prepared = array(); @@ -250,7 +209,7 @@ class Problem } /** - * Turns a constraint into text usable in a sentence describing a job + * Turns a constraint into text usable in a sentence describing a request * * @param \Composer\Semver\Constraint\ConstraintInterface $constraint * @return string diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 851407591..e492e35a4 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -24,7 +24,7 @@ use Composer\Semver\Constraint\ConstraintInterface; class Request { protected $lockedRepository; - protected $jobs = array(); + protected $requires = array(); protected $fixedPackages = array(); protected $unlockables = array(); @@ -33,9 +33,10 @@ class Request $this->lockedRepository = $lockedRepository; } - public function install($packageName, ConstraintInterface $constraint = null) + public function require($packageName, ConstraintInterface $constraint = null) { - $this->addJob($packageName, 'install', $constraint); + $packageName = strtolower($packageName); + $this->requires[$packageName] = $constraint; } /** @@ -52,20 +53,9 @@ class Request } } - protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null) + public function getRequires() { - $packageName = strtolower($packageName); - - $this->jobs[] = array( - 'cmd' => $cmd, - 'packageName' => $packageName, - 'constraint' => $constraint, - ); - } - - public function getJobs() - { - return $this->jobs; + return $this->requires; } public function getFixedPackages() diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 7a12664db..fa4a0f574 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -24,8 +24,8 @@ abstract class Rule { // reason constants const RULE_INTERNAL_ALLOW_UPDATE = 1; - const RULE_JOB_INSTALL = 2; - const RULE_JOB_REMOVE = 3; + const RULE_ROOT_REQUIRE = 2; + const RULE_FIXED = 3; const RULE_PACKAGE_CONFLICT = 6; const RULE_PACKAGE_REQUIRES = 7; const RULE_PACKAGE_OBSOLETES = 8; @@ -41,22 +41,17 @@ abstract class Rule const BITFIELD_DISABLED = 16; protected $bitfield; - protected $job; + protected $request; protected $reasonData; /** * @param int $reason A RULE_* constant describing the reason for generating this rule * @param Link|PackageInterface $reasonData - * @param array $job The job this rule was created from */ - public function __construct($reason, $reasonData, $job = null) + public function __construct($reason, $reasonData) { $this->reasonData = $reasonData; - if ($job) { - $this->job = $job; - } - $this->bitfield = (0 << self::BITFIELD_DISABLED) | ($reason << self::BITFIELD_REASON) | (255 << self::BITFIELD_TYPE); @@ -66,11 +61,6 @@ abstract class Rule abstract public function getHash(); - public function getJob() - { - return $this->job; - } - abstract public function equals(Rule $rule); public function getReason() @@ -85,11 +75,17 @@ abstract class Rule public function getRequiredPackage() { - if ($this->getReason() === self::RULE_JOB_INSTALL) { - return $this->reasonData; + $reason = $this->getReason(); + + if ($reason === self::RULE_ROOT_REQUIRE) { + return $this->reasonData['packageName']; } - if ($this->getReason() === self::RULE_PACKAGE_REQUIRES) { + if ($reason === self::RULE_FIXED) { + return $this->reasonData['package']->getName(); + } + + if ($reason === self::RULE_PACKAGE_REQUIRES) { return $this->reasonData->getTarget(); } } @@ -142,11 +138,24 @@ abstract class Rule case self::RULE_INTERNAL_ALLOW_UPDATE: return $ruleText; - case self::RULE_JOB_INSTALL: - return "Install command rule ($ruleText)"; + case self::RULE_ROOT_REQUIRE: + $packageName = $this->reasonData['packageName']; + $constraint = $this->reasonData['constraint']; - case self::RULE_JOB_REMOVE: - return "Remove command rule ($ruleText)"; + $packages = $pool->whatProvides($packageName, $constraint); + if (!$packages) { + return 'No package found to satisfy root composer.json require '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : ''); + } + + return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages).'.'; + + case self::RULE_FIXED: + $package = $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'; case self::RULE_PACKAGE_CONFLICT: $package1 = $pool->literalToPackage($literals[0]); @@ -263,6 +272,8 @@ abstract class Rule */ protected function formatPackagesUnique($pool, array $packages) { + // TODO this is essentially a duplicate of Problem: getPackageList, maintain in one place only? + $prepared = array(); foreach ($packages as $package) { if (!is_object($package)) { diff --git a/src/Composer/DependencyResolver/Rule2Literals.php b/src/Composer/DependencyResolver/Rule2Literals.php index 6bf47db34..2df95e09d 100644 --- a/src/Composer/DependencyResolver/Rule2Literals.php +++ b/src/Composer/DependencyResolver/Rule2Literals.php @@ -28,11 +28,10 @@ class Rule2Literals extends Rule * @param int $literal2 * @param int $reason A RULE_* constant describing the reason for generating this rule * @param Link|PackageInterface $reasonData - * @param array $job The job this rule was created from */ - public function __construct($literal1, $literal2, $reason, $reasonData, $job = null) + public function __construct($literal1, $literal2, $reason, $reasonData) { - parent::__construct($reason, $reasonData, $job); + parent::__construct($reason, $reasonData); if ($literal1 < $literal2) { $this->literal1 = $literal1; diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index bf4de0d7c..3db54d9df 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -19,7 +19,7 @@ class RuleSet implements \IteratorAggregate, \Countable { // highest priority => lowest number const TYPE_PACKAGE = 0; - const TYPE_JOB = 1; + const TYPE_REQUEST = 1; const TYPE_LEARNED = 4; /** @@ -32,7 +32,7 @@ class RuleSet implements \IteratorAggregate, \Countable protected static $types = array( 255 => 'UNKNOWN', self::TYPE_PACKAGE => 'PACKAGE', - self::TYPE_JOB => 'JOB', + self::TYPE_REQUEST => 'REQUEST', self::TYPE_LEARNED => 'LEARNED', ); diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 01b75e712..02afdfbf7 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -76,33 +76,17 @@ class RuleSetGenerator * @param array $packages The set of packages to choose from * @param int $reason A RULE_* constant describing the reason for * generating this rule - * @param array $job The job this rule was created from + * @param array $reasonData Additional data like the root require or fix request info * @return Rule The generated rule */ - protected function createInstallOneOfRule(array $packages, $reason, $job) + protected function createInstallOneOfRule(array $packages, $reason, $reasonData) { $literals = array(); foreach ($packages as $package) { $literals[] = $package->id; } - return new GenericRule($literals, $reason, $job['packageName'], $job); - } - - /** - * Creates a rule to remove a package - * - * The rule for a package A is (-A). - * - * @param PackageInterface $package The package to be removed - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param array $job The job this rule was created from - * @return Rule The generated rule - */ - protected function createRemoveRule(PackageInterface $package, $reason, $job) - { - return new GenericRule(array(-$package->id), $reason, $job['packageName'], $job); + return new GenericRule($literals, $reason, $reasonData); } /** @@ -302,34 +286,29 @@ class RuleSetGenerator $this->addRulesForPackage($package, $ignorePlatformReqs); - $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_JOB_INSTALL, array( - 'cmd' => 'fix', - 'packageName' => $package->getName(), - 'constraint' => null, + $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_FIXED, array( 'package' => $package, 'lockable' => !isset($unlockableMap[$package->id]), - 'fixed' => true )); - $this->addRule(RuleSet::TYPE_JOB, $rule); + $this->addRule(RuleSet::TYPE_REQUEST, $rule); } - foreach ($request->getJobs() as $job) { - switch ($job['cmd']) { - case 'install': - if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { - break; - } + foreach ($request->getRequires() as $packageName => $constraint) { + if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) { + continue; + } - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); - if ($packages) { - foreach ($packages as $package) { - $this->addRulesForPackage($package, $ignorePlatformReqs); - } + $packages = $this->pool->whatProvides($packageName, $constraint); + if ($packages) { + foreach ($packages as $package) { + $this->addRulesForPackage($package, $ignorePlatformReqs); + } - $rule = $this->createInstallOneOfRule($packages, Rule::RULE_JOB_INSTALL, $job); - $this->addRule(RuleSet::TYPE_JOB, $rule); - } - break; + $rule = $this->createInstallOneOfRule($packages, Rule::RULE_ROOT_REQUIRE, array( + 'packageName' => $packageName, + 'constraint' => $constraint, + )); + $this->addRule(RuleSet::TYPE_REQUEST, $rule); } } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 08482a14f..be9494d91 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -124,19 +124,19 @@ class Solver $problem->addRule($rule); $problem->addRule($conflict); - $this->disableProblem($rule); + $rule->disable(); $this->problems[] = $problem; continue; } - // conflict with another job + // conflict with another root require/fixed package $problem = new Problem($this->pool); $problem->addRule($rule); $problem->addRule($conflict); - // push all of our rules (can only be job rules) + // push all of our rules (can only be root require/fixed package rules) // asserting this literal on the problem stack - foreach ($this->rules->getIteratorFor(RuleSet::TYPE_JOB) as $assertRule) { + foreach ($this->rules->getIteratorFor(RuleSet::TYPE_REQUEST) as $assertRule) { if ($assertRule->isDisabled() || !$assertRule->isAssertion()) { continue; } @@ -148,7 +148,7 @@ class Solver continue; } $problem->addRule($assertRule); - $this->disableProblem($assertRule); + $assertRule->disable(); } $this->problems[] = $problem; @@ -171,19 +171,15 @@ class Solver */ protected function checkForRootRequireProblems($request, $ignorePlatformReqs) { - foreach ($request->getJobs() as $job) { - switch ($job['cmd']) { - case 'install': - if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { - break; - } + foreach ($request->getRequires() as $packageName => $constraint) { + if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) { + continue; + } - if (!$this->pool->whatProvides($job['packageName'], $job['constraint'])) { - $problem = new Problem($this->pool); - $problem->addRule(new GenericRule(array(), null, null, $job)); - $this->problems[] = $problem; - } - break; + if (!$this->pool->whatProvides($packageName, $constraint)) { + $problem = new Problem($this->pool); + $problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint))); + $this->problems[] = $problem; } } } @@ -208,7 +204,7 @@ class Solver $this->watchGraph->insert(new RuleWatchNode($rule)); } - /* make decisions based on job/update assertions */ + /* make decisions based on root require/fix assertions */ $this->makeAssertionRuleDecisions(); $this->io->writeError('Resolving dependencies through SAT', true, IOInterface::DEBUG); @@ -562,28 +558,6 @@ class Solver return 0; } - /** - * @param Rule $why - */ - private function disableProblem(Rule $why) - { - $job = $why->getJob(); - - if (!$job) { - $why->disable(); - - return; - } - - // disable all rules of this job - foreach ($this->rules as $rule) { - /** @var Rule $rule */ - if ($job === $rule->getJob()) { - $rule->disable(); - } - } - } - private function resetSolver() { $this->decisions->reset(); @@ -631,7 +605,7 @@ class Solver /* * here's the main loop: * 1) propagate new decisions (only needed once) - * 2) fulfill jobs + * 2) fulfill root requires/fixed packages * 3) fulfill all unresolved rules * 4) minimalize solution if we had choices * if we encounter a problem, we rewind to a safe level and restart @@ -657,9 +631,9 @@ class Solver } } - // handle job rules + // handle root require/fixed package rules if ($level < $systemLevel) { - $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_JOB); + $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_REQUEST); foreach ($iterator as $rule) { if ($rule->isEnabled()) { $decisionQueue = array(); @@ -704,7 +678,7 @@ class Solver $systemLevel = $level + 1; - // jobs left + // root requires/fixed packages left $iterator->next(); if ($iterator->valid()) { continue; diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 0184bba9c..f720aba4e 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -78,8 +78,8 @@ class SolverProblemsException extends \RuntimeException private function hasExtensionProblems(array $reasonSets) { foreach ($reasonSets as $reasonSet) { - foreach ($reasonSet as $reason) { - if (isset($reason["rule"]) && 0 === strpos($reason["rule"]->getRequiredPackage(), 'ext-')) { + foreach ($reasonSet as $rule) { + if (0 === strpos($rule->getRequiredPackage(), 'ext-')) { return true; } } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index b8e4499de..6f7cc79fb 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -170,7 +170,7 @@ class Transaction // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? /* - if ('update' === $jobType) { + if ('update' === $opType) { $targetPackage = $operation->getTargetPackage(); if ($targetPackage->isDev()) { $initialPackage = $operation->getInitialPackage(); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9c6c5a719..d51cdf466 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -371,11 +371,11 @@ class Installer // if we're updating mirrors we want to keep exactly the same versions installed which are in the lock file, but we want current remote metadata if ($this->updateMirrors) { foreach ($lockedRepository->getPackages() as $lockedPackage) { - $request->install($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); + $request->require($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); } } else { foreach ($links as $link) { - $request->install($link->getTarget(), $link->getConstraint()); + $request->require($link->getTarget(), $link->getConstraint()); } } @@ -464,13 +464,12 @@ class Installer foreach ($lockTransaction->getOperations() as $operation) { // collect suggestions - $jobType = $operation->getJobType(); if ($operation instanceof InstallOperation) { $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); } // output op, but alias op only in debug verbosity - if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { + if (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(true)); } } @@ -524,7 +523,7 @@ class Installer $links = $this->package->getRequires(); foreach ($links as $link) { - $request->install($link->getTarget(), $link->getConstraint()); + $request->require($link->getTarget(), $link->getConstraint()); } $pool = $repositorySet->createPool($request); @@ -582,7 +581,7 @@ class Installer } foreach ($this->locker->getPlatformRequirements($this->devMode) as $link) { - $request->install($link->getTarget(), $link->getConstraint()); + $request->require($link->getTarget(), $link->getConstraint()); } //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); @@ -656,7 +655,7 @@ class Installer } else { foreach ($localRepoTransaction->getOperations() as $operation) { // output op, but alias op only in debug verbosity - if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { + if (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(false)); } } diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 8d1579b63..40d2a7f83 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -173,14 +173,14 @@ class InstallationManager $promises = array(); foreach ($operations as $operation) { - $jobType = $operation->getJobType(); + $opType = $operation->getOperationType(); $promise = null; - if ($jobType === 'install') { + if ($opType === 'install') { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); $promise = $installer->download($package); - } elseif ($jobType === 'update') { + } elseif ($opType === 'update') { $target = $operation->getTargetPackage(); $targetType = $target->getType(); $installer = $this->getInstaller($targetType); @@ -197,29 +197,29 @@ class InstallationManager } foreach ($operations as $operation) { - $jobType = $operation->getJobType(); + $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything - if (!in_array($jobType, array('update', 'install', 'uninstall'))) { + if (!in_array($opType, array('update', 'install', 'uninstall'))) { // output alias ops in debug verbosity as they have no output otherwise if ($this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(false)); } - $this->$jobType($repo, $operation); + $this->$opType($repo, $operation); continue; } - if ($jobType === 'install' || $jobType === 'uninstall') { + if ($opType === 'install' || $opType === 'uninstall') { $package = $operation->getPackage(); $initialPackage = null; - } elseif ($jobType === 'update') { + } elseif ($opType === 'update') { $package = $operation->getTargetPackage(); $initialPackage = $operation->getInitialPackage(); } $installer = $this->getInstaller($package->getType()); - $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($jobType); + $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType); if (defined($event) && $runScripts && $this->eventDispatcher) { $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); } @@ -229,26 +229,26 @@ class InstallationManager $loop = $this->loop; $io = $this->io; - $promise = $installer->prepare($jobType, $package, $initialPackage); + $promise = $installer->prepare($opType, $package, $initialPackage); if (null === $promise) { $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); } - $promise = $promise->then(function () use ($jobType, $installManager, $repo, $operation) { - return $installManager->$jobType($repo, $operation); - })->then(function () use ($jobType, $installer, $package, $initialPackage) { - return $installer->cleanup($jobType, $package, $initialPackage); - })->then(function () use ($jobType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { + $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { + return $installManager->$opType($repo, $operation); + })->then(function () use ($opType, $installer, $package, $initialPackage) { + return $installer->cleanup($opType, $package, $initialPackage); + })->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { $repo->write($devMode, $installManager); - $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); + $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType); if (defined($event) && $runScripts && $dispatcher) { $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); } - }, function ($e) use ($jobType, $installer, $package, $initialPackage, $loop, $io) { - $io->writeError(' ' . ucfirst($jobType) .' of '.$package->getPrettyName().' failed'); + }, function ($e) use ($opType, $installer, $package, $initialPackage, $loop, $io) { + $io->writeError(' ' . ucfirst($opType) .' of '.$package->getPrettyName().' failed'); - $promise = $installer->cleanup($jobType, $package, $initialPackage); + $promise = $installer->cleanup($opType, $package, $initialPackage); if ($promise) { $loop->wait(array($promise)); } diff --git a/src/Composer/Package/Link.php b/src/Composer/Package/Link.php index 217da0713..5a6c683cc 100644 --- a/src/Composer/Package/Link.php +++ b/src/Composer/Package/Link.php @@ -123,6 +123,6 @@ class Link */ public function getPrettyString(PackageInterface $sourcePackage) { - return $sourcePackage->getPrettyString().' '.$this->description.' '.$this->target.' '.$this->constraint->getPrettyString().''; + return $sourcePackage->getPrettyString().' '.$this->description.' '.$this->target.($this->constraint ? ' '.$this->constraint->getPrettyString() : ''); } } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 9ca6e98e3..1a0fcc0bf 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -157,7 +157,7 @@ class RepositorySet $request = new Request($lockedRepo); foreach ($packageNames as $packageName) { - $request->install($packageName); + $request->require($packageName); } return $this->createPool($request); diff --git a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php index 54a5e8e3c..a06f0b998 100644 --- a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php @@ -82,7 +82,7 @@ class PoolBuilderTest extends TestCase $request = new Request(); foreach ($requestData as $package => $constraint) { - $request->install($package, $parser->parseConstraints($constraint)); + $request->require($package, $parser->parseConstraints($constraint)); } foreach ($fixed as $fixedPackage) { diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index 7534f9a5c..f47456299 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -30,13 +30,13 @@ class RequestTest extends TestCase $repo->addPackage($foobar); $request = new Request(); - $request->install('foo'); + $request->require('foo'); $this->assertEquals( array( - array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null), + 'foo' => null, ), - $request->getJobs() + $request->getRequires() ); } @@ -52,13 +52,13 @@ class RequestTest extends TestCase $repo2->addPackage($foo2); $request = new Request(); - $request->install('foo', $constraint = $this->getVersionConstraint('=', '1')); + $request->require('foo', $constraint = $this->getVersionConstraint('=', '1')); $this->assertEquals( array( - array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint), + 'foo' => $constraint, ), - $request->getJobs() + $request->getRequires() ); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index 783734065..a80c5aea2 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -30,9 +30,9 @@ class RuleSetIteratorTest extends TestCase $this->pool = new Pool(); $this->rules = array( - RuleSet::TYPE_JOB => array( - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), + RuleSet::TYPE_REQUEST => array( + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), ), RuleSet::TYPE_LEARNED => array( new GenericRule(array(), Rule::RULE_INTERNAL_ALLOW_UPDATE, null), @@ -51,8 +51,8 @@ class RuleSetIteratorTest extends TestCase } $expected = array( - $this->rules[RuleSet::TYPE_JOB][0], - $this->rules[RuleSet::TYPE_JOB][1], + $this->rules[RuleSet::TYPE_REQUEST][0], + $this->rules[RuleSet::TYPE_REQUEST][1], $this->rules[RuleSet::TYPE_LEARNED][0], ); @@ -69,8 +69,8 @@ class RuleSetIteratorTest extends TestCase } $expected = array( - RuleSet::TYPE_JOB, - RuleSet::TYPE_JOB, + RuleSet::TYPE_REQUEST, + RuleSet::TYPE_REQUEST, RuleSet::TYPE_LEARNED, ); diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 290b3af2f..2215c019b 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -26,9 +26,9 @@ class RuleSetTest extends TestCase { $rules = array( RuleSet::TYPE_PACKAGE => array(), - RuleSet::TYPE_JOB => array( - new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null), - new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null), + RuleSet::TYPE_REQUEST => array( + new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null), + new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null), ), RuleSet::TYPE_LEARNED => array( new GenericRule(array(), Rule::RULE_INTERNAL_ALLOW_UPDATE, null), @@ -37,9 +37,9 @@ class RuleSetTest extends TestCase $ruleSet = new RuleSet; - $ruleSet->add($rules[RuleSet::TYPE_JOB][0], RuleSet::TYPE_JOB); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][0], RuleSet::TYPE_REQUEST); $ruleSet->add($rules[RuleSet::TYPE_LEARNED][0], RuleSet::TYPE_LEARNED); - $ruleSet->add($rules[RuleSet::TYPE_JOB][1], RuleSet::TYPE_JOB); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][1], RuleSet::TYPE_REQUEST); $this->assertEquals($rules, $ruleSet->getRules()); } @@ -47,20 +47,20 @@ class RuleSetTest extends TestCase public function testAddIgnoresDuplicates() { $rules = array( - RuleSet::TYPE_JOB => array( - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), - new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), + RuleSet::TYPE_REQUEST => array( + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), + new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), ), ); $ruleSet = new RuleSet; - $ruleSet->add($rules[RuleSet::TYPE_JOB][0], RuleSet::TYPE_JOB); - $ruleSet->add($rules[RuleSet::TYPE_JOB][1], RuleSet::TYPE_JOB); - $ruleSet->add($rules[RuleSet::TYPE_JOB][2], RuleSet::TYPE_JOB); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][0], RuleSet::TYPE_REQUEST); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][1], RuleSet::TYPE_REQUEST); + $ruleSet->add($rules[RuleSet::TYPE_REQUEST][2], RuleSet::TYPE_REQUEST); - $this->assertCount(1, $ruleSet->getIteratorFor(array(RuleSet::TYPE_JOB))); + $this->assertCount(1, $ruleSet->getIteratorFor(array(RuleSet::TYPE_REQUEST))); } /** @@ -70,15 +70,15 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $ruleSet->add(new GenericRule(array(), Rule::RULE_JOB_INSTALL, null), 7); + $ruleSet->add(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), 7); } public function testCount() { $ruleSet = new RuleSet; - $ruleSet->add(new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null), RuleSet::TYPE_JOB); - $ruleSet->add(new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null), RuleSet::TYPE_JOB); + $ruleSet->add(new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null), RuleSet::TYPE_REQUEST); + $ruleSet->add(new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null), RuleSet::TYPE_REQUEST); $this->assertEquals(2, $ruleSet->count()); } @@ -87,8 +87,8 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $rule = new GenericRule(array(), Rule::RULE_JOB_INSTALL, null); - $ruleSet->add($rule, RuleSet::TYPE_JOB); + $rule = new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null); + $ruleSet->add($rule, RuleSet::TYPE_REQUEST); $this->assertSame($rule, $ruleSet->ruleById[0]); } @@ -97,9 +97,9 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $rule1 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null); - $ruleSet->add($rule1, RuleSet::TYPE_JOB); + $rule1 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null); + $ruleSet->add($rule1, RuleSet::TYPE_REQUEST); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); $iterator = $ruleSet->getIterator(); @@ -112,10 +112,10 @@ class RuleSetTest extends TestCase public function testGetIteratorFor() { $ruleSet = new RuleSet; - $rule1 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null); + $rule1 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null); - $ruleSet->add($rule1, RuleSet::TYPE_JOB); + $ruleSet->add($rule1, RuleSet::TYPE_REQUEST); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); $iterator = $ruleSet->getIteratorFor(RuleSet::TYPE_LEARNED); @@ -126,13 +126,13 @@ class RuleSetTest extends TestCase public function testGetIteratorWithout() { $ruleSet = new RuleSet; - $rule1 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(2), Rule::RULE_JOB_INSTALL, null); + $rule1 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null); - $ruleSet->add($rule1, RuleSet::TYPE_JOB); + $ruleSet->add($rule1, RuleSet::TYPE_REQUEST); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); - $iterator = $ruleSet->getIteratorWithout(RuleSet::TYPE_JOB); + $iterator = $ruleSet->getIteratorWithout(RuleSet::TYPE_REQUEST); $this->assertSame($rule2, $iterator->current()); } @@ -145,10 +145,10 @@ class RuleSetTest extends TestCase $ruleSet = new RuleSet; $literal = $p->getId(); - $rule = new GenericRule(array($literal), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array($literal), Rule::RULE_ROOT_REQUIRE, array('packageName' => 'foo/bar', 'constraint' => null)); - $ruleSet->add($rule, RuleSet::TYPE_JOB); + $ruleSet->add($rule, RuleSet::TYPE_REQUEST); - $this->assertContains('JOB : Install command rule (install foo 2.1)', $ruleSet->getPrettyString($pool)); + $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 8941f430e..6a01ff2f2 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -17,6 +17,7 @@ use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\Pool; use Composer\Package\BasePackage; +use Composer\Package\Link; use Composer\Repository\ArrayRepository; use Composer\Test\TestCase; @@ -24,7 +25,7 @@ class RuleTest extends TestCase { public function testGetHash() { - $rule = new GenericRule(array(123), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(123), Rule::RULE_ROOT_REQUIRE, null); $hash = unpack('ihash', md5('123', true)); $this->assertEquals($hash['hash'], $rule->getHash()); @@ -32,39 +33,39 @@ class RuleTest extends TestCase public function testEqualsForRulesWithDifferentHashes() { - $rule = new GenericRule(array(1, 2), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(1, 3), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(1, 2), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(1, 3), Rule::RULE_ROOT_REQUIRE, null); $this->assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithDifferLiteralsQuantity() { - $rule = new GenericRule(array(1, 12), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(1, 12), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); $this->assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithSameLiterals() { - $rule = new GenericRule(array(1, 12), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(1, 12), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(1, 12), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(1, 12), Rule::RULE_ROOT_REQUIRE, null); $this->assertTrue($rule->equals($rule2)); } public function testSetAndGetType() { - $rule = new GenericRule(array(), Rule::RULE_JOB_INSTALL, null); - $rule->setType(RuleSet::TYPE_JOB); + $rule = new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null); + $rule->setType(RuleSet::TYPE_REQUEST); - $this->assertEquals(RuleSet::TYPE_JOB, $rule->getType()); + $this->assertEquals(RuleSet::TYPE_REQUEST, $rule->getType()); } public function testEnable() { - $rule = new GenericRule(array(), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null); $rule->disable(); $rule->enable(); @@ -74,7 +75,7 @@ class RuleTest extends TestCase public function testDisable() { - $rule = new GenericRule(array(), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null); $rule->enable(); $rule->disable(); @@ -84,8 +85,8 @@ class RuleTest extends TestCase public function testIsAssertions() { - $rule = new GenericRule(array(1, 12), Rule::RULE_JOB_INSTALL, null); - $rule2 = new GenericRule(array(1), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array(1, 12), Rule::RULE_ROOT_REQUIRE, null); + $rule2 = new GenericRule(array(1), Rule::RULE_ROOT_REQUIRE, null); $this->assertFalse($rule->isAssertion()); $this->assertTrue($rule2->isAssertion()); @@ -98,8 +99,8 @@ class RuleTest extends TestCase $p2 = $this->getPackage('baz', '1.1'), )); - $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_JOB_INSTALL, null); + $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo')); - $this->assertEquals('Install command rule (don\'t install baz 1.1|install foo 2.1)', $rule->getPrettyString($pool)); + $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 01a518f27..255119cc4 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -50,7 +50,7 @@ class SolverTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -72,7 +72,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('B', $this->getVersionConstraint('==', '1')); + $this->request->require('B', $this->getVersionConstraint('==', '1')); $this->createSolver(); try { @@ -97,7 +97,7 @@ class SolverTest extends TestCase $this->repoSet->addRepository($repo1); $this->repoSet->addRepository($repo2); - $this->request->install('foo'); + $this->request->require('foo'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $foo1), @@ -114,7 +114,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -140,7 +140,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $newPackageB11), @@ -164,9 +164,9 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('B'); - $this->request->install('C'); + $this->request->require('A'); + $this->request->require('B'); + $this->request->require('C'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -206,7 +206,7 @@ class SolverTest extends TestCase $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires'))); $this->request->fixPackage($packageA); - $this->request->install('B', $this->getVersionConstraint('=', '1.1.0.0')); + $this->request->require('B', $this->getVersionConstraint('=', '1.1.0.0')); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -219,7 +219,7 @@ class SolverTest extends TestCase $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageA, 'to' => $newPackageA), @@ -238,7 +238,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -252,7 +252,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array()); } @@ -266,7 +266,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->request->fixPackage($packageB); $this->checkSolverResult(array( @@ -281,7 +281,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); + $this->request->require('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -297,7 +297,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); + $this->request->require('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -314,7 +314,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); + $this->request->require('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array( array( @@ -343,8 +343,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('C'); + $this->request->require('A'); + $this->request->require('C'); $this->checkSolverResult(array( array('job' => 'remove', 'package' => $packageD), @@ -365,7 +365,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $middlePackageB), @@ -381,7 +381,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('B'); + $this->request->require('B'); $this->checkSolverResult(array( array('job' => 'remove', 'package' => $packageA), @@ -396,7 +396,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -412,7 +412,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); // must explicitly pick the provider, so error in this case $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); @@ -430,7 +430,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -447,7 +447,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); $this->createSolver(); @@ -464,8 +464,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('Q'); + $this->request->require('A'); + $this->request->require('Q'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageQ), @@ -502,7 +502,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('X'); + $this->request->require('X'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $newPackageB), @@ -521,7 +521,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB2), @@ -545,8 +545,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('C'); + $this->request->require('A'); + $this->request->require('C'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -583,8 +583,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('D'); + $this->request->require('A'); + $this->request->require('D'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageD2), @@ -619,7 +619,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('C', $this->getVersionConstraint('==', '2.0.0.0-dev')); + $this->request->require('C', $this->getVersionConstraint('==', '2.0.0.0-dev')); $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); @@ -637,8 +637,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); - $this->request->install('B'); + $this->request->require('A'); + $this->request->require('B'); $this->createSolver(); try { @@ -650,9 +650,9 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; - $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; + $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $msg .= " - B 1.0 conflicts with A[1.0].\n"; - $msg .= " - Installation request for b -> satisfiable by B[1.0].\n"; + $msg .= " - Root composer.json requires b -> satisfiable by B[1.0].\n"; $this->assertEquals($msg, $e->getMessage()); } } @@ -668,7 +668,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->createSolver(); try { @@ -681,7 +681,7 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; - $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; + $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $msg .= " - A 1.0 requires b >= 2.0 -> no matching package found.\n\n"; $msg .= "Potential causes:\n"; $msg .= " - A typo in the package name\n"; @@ -716,7 +716,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->createSolver(); try { @@ -733,7 +733,7 @@ class SolverTest extends TestCase $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; $msg .= " - Same name, can only install one of: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; - $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; + $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $this->assertEquals($msg, $e->getMessage()); } } @@ -756,8 +756,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('symfony/twig-bridge'); - $this->request->install('twig/twig'); + $this->request->require('symfony/twig-bridge'); + $this->request->require('twig/twig'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageTwig16), @@ -782,7 +782,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('==', '1.1.0.0')); + $this->request->require('A', $this->getVersionConstraint('==', '1.1.0.0')); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -804,8 +804,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A', $this->getVersionConstraint('==', '2.0')); - $this->request->install('B'); + $this->request->require('A', $this->getVersionConstraint('==', '2.0')); + $this->request->require('B'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -865,7 +865,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->install('A'); + $this->request->require('A'); $this->createSolver(); @@ -905,14 +905,14 @@ class SolverTest extends TestCase $result = array(); foreach ($transaction->getOperations() as $operation) { - if ('update' === $operation->getJobType()) { + if ('update' === $operation->getOperationType()) { $result[] = array( 'job' => 'update', 'from' => $operation->getInitialPackage(), 'to' => $operation->getTargetPackage(), ); } else { - $job = ('uninstall' === $operation->getJobType() ? 'remove' : 'install'); + $job = ('uninstall' === $operation->getOperationType() ? 'remove' : 'install'); $result[] = array( 'job' => $job, 'package' => $operation->getPackage(), diff --git a/tests/Composer/Test/DependencyResolver/TransactionTest.php b/tests/Composer/Test/DependencyResolver/TransactionTest.php index 6cde5d5d0..8b3e66b68 100644 --- a/tests/Composer/Test/DependencyResolver/TransactionTest.php +++ b/tests/Composer/Test/DependencyResolver/TransactionTest.php @@ -71,7 +71,7 @@ class TransactionTest extends TestCase { $result = array(); foreach ($transaction->getOperations() as $operation) { - if ('update' === $operation->getJobType()) { + if ('update' === $operation->getOperationType()) { $result[] = array( 'job' => 'update', 'from' => $operation->getInitialPackage(), @@ -79,7 +79,7 @@ class TransactionTest extends TestCase ); } else { $result[] = array( - 'job' => $operation->getJobType(), + 'job' => $operation->getOperationType(), 'package' => $operation->getPackage(), ); } diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index 3d5cba664..d970d5c4c 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -28,7 +28,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - c/c 1.0.0 requires x/x 1.0 -> no matching package found. - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0]. - - Installation request for b/b 1.* -> satisfiable by b/b[1.0.0]. + - Root composer.json requires b/b 1.* -> satisfiable by b/b[1.0.0]. Potential causes: - A typo in the package name diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index 2fbd8784c..7d012cf7c 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -36,7 +36,7 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Installation request for a/a ~1.0 -> satisfiable by a/a[1.0.0]. + - Root composer.json requires a/a ~1.0 -> satisfiable by a/a[1.0.0]. - a/a 1.0.0 requires php 5.5 -> your PHP version (%s) overridden by "config.platform.php" version (5.3) does not satisfy that requirement. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 7ca5b628f..a0264257f 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -63,7 +63,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 3 - The requested package stable-requiree-excluded/pkg could not be found in any version, there may be a typo in the package name. Problem 4 - - Installation request for requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. Potential causes: diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index c189e9173..220313daa 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -38,7 +38,7 @@ class InstallationManagerMock extends InstallationManager public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) { foreach ($operations as $operation) { - $method = $operation->getJobType(); + $method = $operation->getOperationType(); // skipping download() step here for tests $this->$method($repo, $operation); } From 0b4e8952f6ba17a4f20b4e81d0e49ee4d7a866a4 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sun, 19 Jan 2020 23:28:00 +0100 Subject: [PATCH 198/321] PHP5 disallows require as a function name, use requireName analog to fixPackage --- src/Composer/DependencyResolver/Request.php | 2 +- src/Composer/Installer.php | 8 +- src/Composer/Repository/RepositorySet.php | 2 +- .../DependencyResolver/PoolBuilderTest.php | 2 +- .../Test/DependencyResolver/RequestTest.php | 4 +- .../Test/DependencyResolver/SolverTest.php | 86 +++++++++---------- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index e492e35a4..d4f1b0523 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -33,7 +33,7 @@ class Request $this->lockedRepository = $lockedRepository; } - public function require($packageName, ConstraintInterface $constraint = null) + public function requireName($packageName, ConstraintInterface $constraint = null) { $packageName = strtolower($packageName); $this->requires[$packageName] = $constraint; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index d51cdf466..c2234f27c 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -371,11 +371,11 @@ class Installer // if we're updating mirrors we want to keep exactly the same versions installed which are in the lock file, but we want current remote metadata if ($this->updateMirrors) { foreach ($lockedRepository->getPackages() as $lockedPackage) { - $request->require($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); + $request->requireName($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); } } else { foreach ($links as $link) { - $request->require($link->getTarget(), $link->getConstraint()); + $request->requireName($link->getTarget(), $link->getConstraint()); } } @@ -523,7 +523,7 @@ class Installer $links = $this->package->getRequires(); foreach ($links as $link) { - $request->require($link->getTarget(), $link->getConstraint()); + $request->requireName($link->getTarget(), $link->getConstraint()); } $pool = $repositorySet->createPool($request); @@ -581,7 +581,7 @@ class Installer } foreach ($this->locker->getPlatformRequirements($this->devMode) as $link) { - $request->require($link->getTarget(), $link->getConstraint()); + $request->requireName($link->getTarget(), $link->getConstraint()); } //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 1a0fcc0bf..10f3b7dd6 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -157,7 +157,7 @@ class RepositorySet $request = new Request($lockedRepo); foreach ($packageNames as $packageName) { - $request->require($packageName); + $request->requireName($packageName); } return $this->createPool($request); diff --git a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php index a06f0b998..05bbe922d 100644 --- a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php @@ -82,7 +82,7 @@ class PoolBuilderTest extends TestCase $request = new Request(); foreach ($requestData as $package => $constraint) { - $request->require($package, $parser->parseConstraints($constraint)); + $request->requireName($package, $parser->parseConstraints($constraint)); } foreach ($fixed as $fixedPackage) { diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index f47456299..e2a3ce9e6 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -30,7 +30,7 @@ class RequestTest extends TestCase $repo->addPackage($foobar); $request = new Request(); - $request->require('foo'); + $request->requireName('foo'); $this->assertEquals( array( @@ -52,7 +52,7 @@ class RequestTest extends TestCase $repo2->addPackage($foo2); $request = new Request(); - $request->require('foo', $constraint = $this->getVersionConstraint('=', '1')); + $request->requireName('foo', $constraint = $this->getVersionConstraint('=', '1')); $this->assertEquals( array( diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 255119cc4..bc9b39204 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -50,7 +50,7 @@ class SolverTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -72,7 +72,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->require('B', $this->getVersionConstraint('==', '1')); + $this->request->requireName('B', $this->getVersionConstraint('==', '1')); $this->createSolver(); try { @@ -97,7 +97,7 @@ class SolverTest extends TestCase $this->repoSet->addRepository($repo1); $this->repoSet->addRepository($repo2); - $this->request->require('foo'); + $this->request->requireName('foo'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $foo1), @@ -114,7 +114,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -140,7 +140,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $newPackageB11), @@ -164,9 +164,9 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); - $this->request->require('B'); - $this->request->require('C'); + $this->request->requireName('A'); + $this->request->requireName('B'); + $this->request->requireName('C'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -206,7 +206,7 @@ class SolverTest extends TestCase $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires'))); $this->request->fixPackage($packageA); - $this->request->require('B', $this->getVersionConstraint('=', '1.1.0.0')); + $this->request->requireName('B', $this->getVersionConstraint('=', '1.1.0.0')); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -219,7 +219,7 @@ class SolverTest extends TestCase $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageA, 'to' => $newPackageA), @@ -238,7 +238,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -252,7 +252,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array()); } @@ -266,7 +266,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->request->fixPackage($packageB); $this->checkSolverResult(array( @@ -281,7 +281,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->require('A', $this->getVersionConstraint('<', '2.0.0.0')); + $this->request->requireName('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -297,7 +297,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->require('A', $this->getVersionConstraint('<', '2.0.0.0')); + $this->request->requireName('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -314,7 +314,7 @@ class SolverTest extends TestCase $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); - $this->request->require('A', $this->getVersionConstraint('<', '2.0.0.0')); + $this->request->requireName('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array( array( @@ -343,8 +343,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); - $this->request->require('C'); + $this->request->requireName('A'); + $this->request->requireName('C'); $this->checkSolverResult(array( array('job' => 'remove', 'package' => $packageD), @@ -365,7 +365,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $middlePackageB), @@ -381,7 +381,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('B'); + $this->request->requireName('B'); $this->checkSolverResult(array( array('job' => 'remove', 'package' => $packageA), @@ -396,7 +396,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -412,7 +412,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); // must explicitly pick the provider, so error in this case $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); @@ -430,7 +430,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -447,7 +447,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); $this->createSolver(); @@ -464,8 +464,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); - $this->request->require('Q'); + $this->request->requireName('A'); + $this->request->requireName('Q'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageQ), @@ -502,7 +502,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('X'); + $this->request->requireName('X'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $newPackageB), @@ -521,7 +521,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB2), @@ -545,8 +545,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); - $this->request->require('C'); + $this->request->requireName('A'); + $this->request->requireName('C'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -583,8 +583,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); - $this->request->require('D'); + $this->request->requireName('A'); + $this->request->requireName('D'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageD2), @@ -619,7 +619,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('C', $this->getVersionConstraint('==', '2.0.0.0-dev')); + $this->request->requireName('C', $this->getVersionConstraint('==', '2.0.0.0-dev')); $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); @@ -637,8 +637,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); - $this->request->require('B'); + $this->request->requireName('A'); + $this->request->requireName('B'); $this->createSolver(); try { @@ -668,7 +668,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->createSolver(); try { @@ -716,7 +716,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->createSolver(); try { @@ -756,8 +756,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('symfony/twig-bridge'); - $this->request->require('twig/twig'); + $this->request->requireName('symfony/twig-bridge'); + $this->request->requireName('twig/twig'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageTwig16), @@ -782,7 +782,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A', $this->getVersionConstraint('==', '1.1.0.0')); + $this->request->requireName('A', $this->getVersionConstraint('==', '1.1.0.0')); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), @@ -804,8 +804,8 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A', $this->getVersionConstraint('==', '2.0')); - $this->request->require('B'); + $this->request->requireName('A', $this->getVersionConstraint('==', '2.0')); + $this->request->requireName('B'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), @@ -865,7 +865,7 @@ class SolverTest extends TestCase $this->reposComplete(); - $this->request->require('A'); + $this->request->requireName('A'); $this->createSolver(); From 8b877eb068d3f021e43757bb6c2031a2bcb12a14 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Mon, 20 Jan 2020 10:27:04 +0100 Subject: [PATCH 199/321] Remove superfluous check in Problem --- src/Composer/DependencyResolver/Problem.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 130681217..5291bdfaa 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -82,7 +82,6 @@ class Problem } $request = $rule->getReasonData(); - $packageName = $request['packageName']; $constraint = $request['constraint']; @@ -92,7 +91,7 @@ class Problem $packages = array(); } - if ($request && empty($packages)) { + if (empty($packages)) { // handle php/hhvm if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { $version = phpversion(); From 173b96de2deaf543e1d69ac5fc46865f8265f462 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 28 Jan 2020 15:13:35 +0100 Subject: [PATCH 200/321] Make sure the first require does not fail due to a missing lock file --- src/Composer/Command/RequireCommand.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 48851c28d..10506170f 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -35,6 +35,7 @@ use Composer\Util\Silencer; class RequireCommand extends InitCommand { private $newlyCreated; + private $firstRequire; private $json; private $file; private $composerBackup; @@ -186,6 +187,14 @@ EOT $sortPackages = $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages'); + $this->firstRequire = $this->newlyCreated; + if (!$this->firstRequire) { + $composerDefinition = $this->json->read(); + if (empty($composerDefinition['require']) && empty($composerDefinition['require-dev'])) { + $this->firstRequire = true; + } + } + if (!$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) { $composerDefinition = $this->json->read(); foreach ($requirements as $package => $version) { @@ -245,7 +254,7 @@ EOT // if no lock is present, or the file is brand new, we do not do a // partial update as this is not supported by the Installer - if (!$this->newlyCreated && $composer->getConfig()->get('lock')) { + if (!$this->firstRequire && $composer->getConfig()->get('lock')) { $install->setUpdateWhitelist(array_keys($requirements)); } From 292d4b3c8f59758fd0de3c486cd480ed9ac6e8bd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 28 Jan 2020 15:14:58 +0100 Subject: [PATCH 201/321] Make sure a lock file is always written and things work well even without any dependencies in the require/require-dev --- src/Composer/Package/Locker.php | 12 ----- .../installer/update-to-empty-from-blank.test | 20 ++++++++ .../update-to-empty-from-locked.test | 47 +++++++++++++++++++ 3 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test create mode 100644 tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 1e5ea8531..f4c252989 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -345,18 +345,6 @@ class Locker $lock['platform-overrides'] = $platformOverrides; } - if (empty($lock['packages']) && empty($lock['packages-dev']) && empty($lock['platform']) && empty($lock['platform-dev'])) { - if ($this->lockFile->exists()) { - if ($write) { - unlink($this->lockFile->getPath()); - } else { - $this->virtualFileWritten = false; - } - } - - return false; - } - try { $isLocked = $this->isLocked(); } catch (ParsingException $e) { diff --git a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test new file mode 100644 index 000000000..e9892c3b6 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test @@ -0,0 +1,20 @@ +--TEST-- +Update to a state without dependency works well from a blank slate +--COMPOSER-- +{ +} +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test new file mode 100644 index 000000000..c7e93ccaf --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test @@ -0,0 +1,47 @@ +--TEST-- +Update to a state without dependency works well from locked with dependency +--COMPOSER-- +{ + "minimum-stability": "dev" +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1234", "type": "git", "url": "" } + } +] +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1234", "type": "git", "url": "" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Uninstalling a/a (dev-master 1234) From 41720e9b70471c9396ae228fafd3ce1e408c53d8 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 28 Jan 2020 15:56:52 +0100 Subject: [PATCH 202/321] Show abandoned warnings from the locked repo --- src/Composer/Installer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 45778a5b0..b933268c0 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -262,9 +262,9 @@ class Installer $this->suggestedPackagesReporter->output($this->locker->getLockedRepository($this->devMode)); } - // TODO probably makes more sense to do this on the lock file only? - # Find abandoned packages and warn user - foreach ($localRepo->getPackages() as $package) { + // Find abandoned packages and warn user + $lockedRepository = $this->locker->getLockedRepository(true); + foreach ($lockedRepository->getPackages() as $package) { if (!$package instanceof CompletePackage || !$package->isAbandoned()) { continue; } From 3f48acf56a2a9ac293563d81af0b269b609e58a2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 28 Jan 2020 16:01:17 +0100 Subject: [PATCH 203/321] Add test checking references update to latest and remove TODO --- src/Composer/Installer.php | 3 -- .../update-reference-picks-latest.test | 31 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index b933268c0..bbd81d997 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -388,9 +388,6 @@ class Installer $pool = $repositorySet->createPool($request); - // TODO ensure that the solver always picks most recent reference for dev packages, so they get updated even when just a new commit is pushed but version is unchanged - // should already be solved by using the remote package in all cases in the pool - // solve dependencies $solver = new Solver($policy, $pool, $this->io); try { diff --git a/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test b/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test new file mode 100644 index 000000000..6b1f540bb --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test @@ -0,0 +1,31 @@ +--TEST-- +Updating a dev package should update to the latest available reference +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "abc123", "url": "", "type": "git" } + } + ] + } + ], + "require": { + "a/a": "dev-master" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "def000", "url": "", "type": "git" }, + "dist": { "reference": "def000", "url": "", "type": "zip", "shasum": "" } + } +] +--RUN-- +update +--EXPECT-- +Updating a/a (dev-master def000 => dev-master abc123) From 39fb2cc51ca8569530e0ecd911bc364ec08b8f48 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 28 Jan 2020 16:04:11 +0100 Subject: [PATCH 204/321] Fix case --- src/Composer/Package/Locker.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index f4c252989..2de846359 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -210,7 +210,7 @@ class Locker if (!empty($lockData['platform'])) { $requirements = $this->loader->parseLinks( - '__ROOT__', + '__root__', '1.0.0', 'requires', isset($lockData['platform']) ? $lockData['platform'] : array() @@ -219,7 +219,7 @@ class Locker if ($withDevReqs && !empty($lockData['platform-dev'])) { $devRequirements = $this->loader->parseLinks( - '__ROOT__', + '__root__', '1.0.0', 'requires', isset($lockData['platform-dev']) ? $lockData['platform-dev'] : array() From d18b5b6f2e771761a5446f59b3d14f6ecc0f333d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 29 Jan 2020 23:13:55 +0100 Subject: [PATCH 205/321] Update phpstan config --- phpstan/config.neon | 4 ---- 1 file changed, 4 deletions(-) diff --git a/phpstan/config.neon b/phpstan/config.neon index 599d27d51..78c45b90e 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -9,10 +9,6 @@ parameters: ignoreErrors: # ion cube is not installed - '~^Function ioncube_loader_\w+ not found\.$~' - # rar is not installed - - '~^Call to static method open\(\) on an unknown class RarArchive\.$~' - # imagick is not installed - - '~^Instantiated class Imagick not found\.$~' # variables from global scope - '~^Undefined variable: \$vendorDir$~' From 8f09f3764b3558ffe7157ec377347c006c47c7e9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 08:40:35 +0100 Subject: [PATCH 206/321] Avoid fetching non-existing files multiple times --- src/Composer/Repository/ComposerRepository.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index b23d5aced..07eff48eb 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -65,6 +65,12 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $rootData; private $hasPartialPackages; private $partialPackagesByName; + + /** + * @var array list of package names which returned a 404 and should not be re-fetched in case loadPackage is called several times + * useful for v2 metadata repositories with lazy providers + */ + private $packagesNotFoundCache = array(); /** * TODO v3 should make this private once we can drop PHP 5.3 support * @private @@ -1079,6 +1085,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito { $retries = 3; + if (isset($this->packagesNotFoundCache[$filename])) { + return \React\Promise\Util::promiseFor(array('packages' => array())); + } + $httpDownloader = $this->httpDownloader; if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); @@ -1095,6 +1105,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $accept = function ($response) use ($io, $url, $cache, $cacheKey) { // package not found is acceptable for a v2 protocol repository if ($response->getStatusCode() === 404) { + $this->packagesNotFoundCache[$filename] = true; return array('packages' => array()); } @@ -1119,6 +1130,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, &$degradedMode) { if ($e instanceof TransportException && $e->getStatusCode() === 404) { + $this->packagesNotFoundCache[$filename] = true; return false; } From 2901995fc8fa282d4f90a04b40475ea9e3f8b44c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 10:17:42 +0100 Subject: [PATCH 207/321] Fix issue excluding unstable packages when stabilityFlags are empty --- src/Composer/Repository/ComposerRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 07eff48eb..626bf119f 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -697,7 +697,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } foreach ($versions as $version) { - if ($acceptableStabilities && $stabilityFlags && !StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), VersionParser::parseStability($version))) { + if (null !== $acceptableStabilities && null !== $stabilityFlags && !StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), VersionParser::parseStability($version))) { continue; } From 8a41f1a5ca1378845dd0925d74648fd24f7b5c5d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 13:19:54 +0100 Subject: [PATCH 208/321] Allow providers which are selected to be installed in place of existing packages which do not satisfy requirements, fixes #6753 --- src/Composer/DependencyResolver/Pool.php | 24 +------- ...packages-can-be-installed-if-selected.test | 36 ++++++++++++ ...her-with-provided-if-both-installable.json | 34 ++++++++++++ ...-can-not-be-installed-unless-selected.test | 55 +++++++++++++++++++ 4 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-if-selected.test create mode 100644 tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-together-with-provided-if-both-installable.json create mode 100644 tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 69a5b2cc2..1e1100b15 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -27,7 +27,6 @@ use Composer\Package\PackageInterface; */ class Pool implements \Countable { - const MATCH_NAME = -1; const MATCH_NONE = 0; const MATCH = 1; const MATCH_PROVIDE = 2; @@ -117,27 +116,15 @@ class Pool implements \Countable $candidates = $this->packageByName[$name]; } - $matches = $provideMatches = array(); - $nameMatch = false; + $matches = array(); foreach ($candidates as $candidate) { switch ($this->match($candidate, $name, $constraint)) { case self::MATCH_NONE: break; - case self::MATCH_NAME: - $nameMatch = true; - break; - case self::MATCH: - $nameMatch = true; - $matches[] = $candidate; - break; - case self::MATCH_PROVIDE: - $provideMatches[] = $candidate; - break; - case self::MATCH_REPLACE: $matches[] = $candidate; break; @@ -147,12 +134,7 @@ class Pool implements \Countable } } - // if a package with the required name exists, we ignore providers - if ($nameMatch) { - return $matches; - } - - return array_merge($matches, $provideMatches); + return $matches; } public function literalToPackage($literal) @@ -196,7 +178,7 @@ class Pool implements \Countable return self::MATCH; } - return self::MATCH_NAME; + return self::MATCH_NONE; } $provides = $candidate->getProvides(); diff --git a/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-if-selected.test b/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-if-selected.test new file mode 100644 index 000000000..be425010c --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-if-selected.test @@ -0,0 +1,36 @@ +--TEST-- +Test that providers can be installed if they are selected and the package they provide is not installable +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "foo/polyfill", + "provide": { + "foo/standard": "1.0.0" + }, + "version": "1.0.0" + }, + { + "name": "foo/standard", + "require": { + "foo/does-not-exist": "1.0.0" + }, + "version": "1.0.0" + } + ] + } + ], + "require": { + "foo/standard": "1.0.0", + "foo/polyfill": "1.0.0" + } +} + +--RUN-- +update + +--EXPECT-- +Installing foo/polyfill (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-together-with-provided-if-both-installable.json b/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-together-with-provided-if-both-installable.json new file mode 100644 index 000000000..9abe90dd8 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-packages-can-be-installed-together-with-provided-if-both-installable.json @@ -0,0 +1,34 @@ +--TEST-- +Test that providers can be installed in conjunction with the package they provide if they are selected and the package they provide is also installable +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "foo/polyfill", + "provide": { + "foo/standard": "1.0.0" + }, + "version": "1.0.0" + }, + { + "name": "foo/standard", + "version": "1.0.0" + } + ] + } + ], + "require": { + "foo/standard": "1.0.0", + "foo/polyfill": "1.0.0" + } +} + +--RUN-- +update + +--EXPECT-- +Installing foo/standard (1.0.0) +Installing foo/polyfill (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test b/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test new file mode 100644 index 000000000..85347b4e6 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test @@ -0,0 +1,55 @@ +--TEST-- +Test that providers can not be installed if they are not selected +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "foo/polyfill", + "provide": { + "foo/standard": "1.0.0" + }, + "version": "1.0.0" + }, + { + "name": "foo/standard", + "require": { + "foo/does-not-exist": "1.0.0" + }, + "version": "1.0.0" + } + ] + } + ], + "require": { + "foo/standard": "1.0.0" + } +} + +--RUN-- +update + +--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 + - Root composer.json requires foo/standard 1.0.0 -> satisfiable by foo/standard[1.0.0]. + - foo/standard 1.0.0 requires foo/does-not-exist 1.0.0 -> no matching package found. + +Potential causes: + - A typo in the package name + - The package is not available in a stable-enough version according to your minimum-stability setting + see for more details. + - It's a private package and you forgot to add a custom repository to find it + +Read for further common problems. + +--EXPECT-- + From 46e35bc8d7027c7355d0950899a8b27f60e53a21 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 14:17:49 +0100 Subject: [PATCH 209/321] Fix 5.3/undef var issues --- src/Composer/Repository/ComposerRepository.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 626bf119f..3919d2a6b 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -34,6 +34,7 @@ use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; use Composer\Util\Http\Response; use Composer\Util\MetadataMinifier; +use React\Promise\Util as PromiseUtil; /** * @author Jordi Boggiano @@ -67,10 +68,12 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $partialPackagesByName; /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * @private * @var array list of package names which returned a 404 and should not be re-fetched in case loadPackage is called several times * useful for v2 metadata repositories with lazy providers */ - private $packagesNotFoundCache = array(); + public $packagesNotFoundCache = array(); /** * TODO v3 should make this private once we can drop PHP 5.3 support * @private @@ -1086,7 +1089,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; if (isset($this->packagesNotFoundCache[$filename])) { - return \React\Promise\Util::promiseFor(array('packages' => array())); + return PromiseUtil::promiseFor(array('packages' => array())); } $httpDownloader = $this->httpDownloader; @@ -1101,11 +1104,12 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $url = $this->url; $cache = $this->cache; $degradedMode =& $this->degradedMode; + $repo = $this; - $accept = function ($response) use ($io, $url, $cache, $cacheKey) { + $accept = function ($response) use ($io, $url, $filename, $cache, $cacheKey, $repo) { // package not found is acceptable for a v2 protocol repository if ($response->getStatusCode() === 404) { - $this->packagesNotFoundCache[$filename] = true; + $repo->packagesNotFoundCache[$filename] = true; return array('packages' => array()); } @@ -1128,9 +1132,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $data; }; - $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, &$degradedMode) { + $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, &$degradedMode, $repo) { if ($e instanceof TransportException && $e->getStatusCode() === 404) { - $this->packagesNotFoundCache[$filename] = true; + $repo->packagesNotFoundCache[$filename] = true; return false; } From 3fc7e10c5c0871d2fceb0de599d3e43a39f19158 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 29 Jan 2020 22:47:16 +0100 Subject: [PATCH 210/321] Improve error reporting of solver issues, refs #7779 Fixes #8525 Fixes #6513 --- .../Command/BaseDependencyCommand.php | 2 +- .../DependencyResolver/PoolBuilder.php | 4 +- src/Composer/DependencyResolver/Problem.php | 241 ++++++++++++------ src/Composer/DependencyResolver/Rule.php | 74 +----- src/Composer/DependencyResolver/RuleSet.php | 8 +- src/Composer/DependencyResolver/Solver.php | 22 +- .../SolverProblemsException.php | 11 +- src/Composer/Installer.php | 12 +- src/Composer/Plugin/PluginManager.php | 2 +- src/Composer/Repository/ArrayRepository.php | 8 + .../Repository/ArtifactRepository.php | 5 + .../Repository/ComposerRepository.php | 5 + .../Repository/CompositeRepository.php | 5 + .../Repository/InstalledArrayRepository.php | 4 + .../InstalledFilesystemRepository.php | 4 + .../Repository/LockArrayRepository.php | 4 + src/Composer/Repository/PackageRepository.php | 5 + src/Composer/Repository/PathRepository.php | 5 + src/Composer/Repository/PearRepository.php | 5 + .../Repository/PlatformRepository.php | 9 +- .../Repository/RepositoryInterface.php | 9 + src/Composer/Repository/RepositorySet.php | 49 +++- .../Repository/RootPackageRepository.php | 4 + src/Composer/Repository/VcsRepository.php | 11 + .../Test/DependencyResolver/RuleSetTest.php | 8 +- .../Test/DependencyResolver/RuleTest.php | 8 +- .../Test/DependencyResolver/SolverTest.php | 12 +- .../installer/broken-deps-do-not-replace.test | 2 +- .../conflict-between-dependents.test | 38 +++ .../installer/github-issues-4319.test | 2 +- ...e-downgrades-non-whitelisted-unstable.test | 12 +- .../installer/repositories-priorities.test | 9 +- .../Fixtures/installer/solver-problems.test | 103 +++++++- tests/Composer/Test/InstallerTest.php | 3 + 34 files changed, 482 insertions(+), 223 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 00fed9240..be7703f02 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -89,7 +89,7 @@ class BaseDependencyCommand extends BaseCommand ); // Find packages that are or provide the requested package first - $packages = $repositorySet->findPackages(strtolower($needle), null, false); + $packages = $repositorySet->findPackages(strtolower($needle), null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); if (empty($packages)) { throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index bcfc67fea..360924143 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -31,7 +31,6 @@ class PoolBuilder private $stabilityFlags; private $rootAliases; private $rootReferences; - private $rootRequires; private $aliasMap = array(); private $nameConstraints = array(); @@ -39,13 +38,12 @@ class PoolBuilder private $packages = array(); private $unacceptableFixedPackages = array(); - public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, array $rootRequires = array()) + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences) { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; - $this->rootRequires = $rootRequires; } public function buildPool(array $repositories, Request $request) diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 5291bdfaa..d91b48dd3 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -13,6 +13,8 @@ namespace Composer\DependencyResolver; use Composer\Package\CompletePackageInterface; +use Composer\Repository\RepositorySet; +use Composer\Semver\Constraint\Constraint; /** * Represents a problem detected while solving dependencies @@ -35,13 +37,6 @@ class Problem protected $section = 0; - protected $pool; - - public function __construct(Pool $pool) - { - $this->pool = $pool; - } - /** * Add a rule as a reason * @@ -68,7 +63,7 @@ class Problem * @param array $installedMap A map of all present packages * @return string */ - public function getPrettyString(array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, array $installedMap = array(), array $learnedPool = array()) { // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections? $reasons = call_user_func_array('array_merge', array_reverse($this->reasons)); @@ -81,91 +76,25 @@ class Problem throw new \LogicException("Single reason problems must contain a request rule."); } - $request = $rule->getReasonData(); - $packageName = $request['packageName']; - $constraint = $request['constraint']; + $reasonData = $rule->getReasonData(); + $packageName = $reasonData['packageName']; + $constraint = $reasonData['constraint']; if (isset($constraint)) { - $packages = $this->pool->whatProvides($packageName, $constraint); + $packages = $repositorySet->getPool()->whatProvides($packageName, $constraint); } else { $packages = array(); } if (empty($packages)) { - // handle php/hhvm - if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { - $version = phpversion(); - $available = $this->pool->whatProvides($packageName); - - if (count($available)) { - $firstAvailable = reset($available); - $version = $firstAvailable->getPrettyVersion(); - $extra = $firstAvailable->getExtra(); - if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { - $version .= '; ' . $firstAvailable->getDescription(); - } - } - - $msg = "\n - This package requires ".$packageName.$this->constraintToText($constraint).' but '; - - if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) { - return $msg . 'your HHVM version does not satisfy that requirement.'; - } - - if ($packageName === 'hhvm') { - return $msg . 'you are running this with PHP and not HHVM.'; - } - - return $msg . 'your PHP version ('. $version .') does not satisfy that requirement.'; - } - - // handle php extensions - if (0 === stripos($packageName, 'ext-')) { - if (false !== strpos($packageName, ' ')) { - return "\n - The requested PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'; - } - - $ext = substr($packageName, 4); - $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system'; - - return "\n - The requested PHP extension ".$packageName.$this->constraintToText($constraint).' '.$error.'. Install or enable PHP\'s '.$ext.' extension.'; - } - - // handle linked libs - if (0 === stripos($packageName, 'lib-')) { - if (strtolower($packageName) === 'lib-icu') { - $error = extension_loaded('intl') ? 'has the wrong version installed, try upgrading the intl extension.' : 'is missing from your system, make sure the intl extension is loaded.'; - - return "\n - The requested linked library ".$packageName.$this->constraintToText($constraint).' '.$error; - } - - return "\n - The requested linked library ".$packageName.$this->constraintToText($constraint).' has the wrong version installed or is missing from your system, make sure to load the extension providing it.'; - } - - if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) { - $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName); - - return "\n - The requested package ".$packageName.' could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'; - } - - // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet - /*if ($providers = $this->pool->whatProvides($packageName, $constraint, true, true)) { - return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' is satisfiable by '.$this->getPackageList($providers).' but these conflict with your requirements or minimum-stability.'; - }*/ - - // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet - /*if ($providers = $this->pool->whatProvides($packageName, null, true, true)) { - return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' exists as '.$this->getPackageList($providers).' but these are rejected by your constraint.'; - }*/ - - return "\n - The requested package ".$packageName.' could not be found in any version, there may be a typo in the package name.'; + return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $packageName, $constraint)); } } $messages = array(); foreach ($reasons as $rule) { - $messages[] = $rule->getPrettyString($this->pool, $installedMap, $learnedPool); + $messages[] = $rule->getPrettyString($repositorySet, $request, $installedMap, $learnedPool); } return "\n - ".implode("\n - ", $messages); @@ -193,7 +122,141 @@ class Problem $this->section++; } - protected function getPackageList($packages) + /** + * @internal + */ + public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, $packageName, $constraint = null) + { + $pool = $repositorySet->getPool(); + + // handle php/hhvm + if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { + $version = phpversion(); + $available = $pool->whatProvides($packageName); + + if (count($available)) { + $firstAvailable = reset($available); + $version = $firstAvailable->getPrettyVersion(); + $extra = $firstAvailable->getExtra(); + if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { + $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription()); + } + } + + $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but '; + + if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) { + return array($msg, 'your HHVM version does not satisfy that requirement.'); + } + + if ($packageName === 'hhvm') { + return array($msg, 'you are running this with PHP and not HHVM.'); + } + + return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.'); + } + + // handle php extensions + if (0 === stripos($packageName, 'ext-')) { + if (false !== strpos($packageName, ' ')) { + return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'); + } + + $ext = substr($packageName, 4); + $error = extension_loaded($ext) ? 'it has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'it is missing from your system'; + + return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.'); + } + + // handle linked libs + if (0 === stripos($packageName, 'lib-')) { + if (strtolower($packageName) === 'lib-icu') { + $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.'; + + return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error); + } + + 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) { + if ($package->getName() === $packageName) { + $fixedPackage = $package; + if ($pool->isUnacceptableFixedPackage($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 whitelist it for update.'); + } + break; + } + } + + // first check if the actual requested package is found in normal conditions + // if so it must mean it is rejected by another constraint than the one given here + if ($packages = $repositorySet->findPackages($packageName, $constraint)) { + $rootReqs = $repositorySet->getRootRequires(); + if (isset($rootReqs[$packageName])) { + $filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) { + return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion())); + }); + if (0 === count($filtered)) { + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').'); + } + } + + if ($fixedPackage) { + $fixedConstraint = new Constraint('==', $fixedPackage->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).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update.'); + } + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.'); + } + + // check if the package is found when bypassing stability checks + if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.'); + } + + // check if the package is found when bypassing the constraint check + if ($packages = $repositorySet->findPackages($packageName, null)) { + // we must first verify if a valid package would be found in a lower priority repository + if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { + $higherRepoPackages = $repositorySet->findPackages($packageName, null); + $nextRepoPackages = array(); + $nextRepo = null; + + foreach ($allReposPackages as $package) { + if ($nextRepo === null || $nextRepo === $package->getRepository()) { + $nextRepoPackages[] = $package; + $nextRepo = $package->getRepository(); + } else { + break; + } + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.'); + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.'); + } + + if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) { + $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName); + + return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'); + } + + return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name."); + } + + /** + * @internal + */ + public static function getPackageList(array $packages) { $prepared = array(); foreach ($packages as $package) { @@ -207,13 +270,27 @@ class Problem return implode(', ', $prepared); } + private static function hasMultipleNames(array $packages) + { + $name = null; + foreach ($packages as $package) { + if ($name === null || $name === $package->getName()) { + $name = $package->getName(); + } else { + return true; + } + } + + return false; + } + /** * Turns a constraint into text usable in a sentence describing a request * * @param \Composer\Semver\Constraint\ConstraintInterface $constraint * @return string */ - protected function constraintToText($constraint) + protected static function constraintToText($constraint) { return $constraint ? ' '.$constraint->getPrettyString() : ''; } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index fa4a0f574..462f3e4df 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -15,6 +15,7 @@ namespace Composer\DependencyResolver; use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\PackageInterface; +use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -122,8 +123,9 @@ abstract class Rule abstract public function isAssertion(); - public function getPrettyString(Pool $pool, array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, array $installedMap = array(), array $learnedPool = array()) { + $pool = $repositorySet->getPool(); $literals = $this->getLiterals(); $ruleText = ''; @@ -178,60 +180,9 @@ abstract class Rule } else { $targetName = $this->reasonData->getTarget(); - if ($targetName === 'php' || $targetName === 'php-64bit' || $targetName === 'hhvm') { - // handle php/hhvm - if (defined('HHVM_VERSION')) { - return $text . ' -> your HHVM version does not satisfy that requirement.'; - } + $reason = Problem::getMissingPackageReason($repositorySet, $request, $targetName, $this->reasonData->getConstraint()); - $packages = $pool->whatProvides($targetName); - $package = count($packages) ? current($packages) : phpversion(); - - if ($targetName === 'hhvm') { - if ($package instanceof CompletePackage) { - return $text . ' -> your HHVM version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } else { - return $text . ' -> you are running this with PHP and not HHVM.'; - } - } - - - if (!($package instanceof CompletePackage)) { - return $text . ' -> your PHP version ('.phpversion().') does not satisfy that requirement.'; - } - - $extra = $package->getExtra(); - - if (!empty($extra['config.platform'])) { - $text .= ' -> your PHP version ('.phpversion().') overridden by "config.platform.php" version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } else { - $text .= ' -> your PHP version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } - - return $text; - } - - if (0 === strpos($targetName, 'ext-')) { - // handle php extensions - $ext = substr($targetName, 4); - $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system'; - - return $text . ' -> the requested PHP extension '.$ext.' '.$error.'.'; - } - - if (0 === strpos($targetName, 'lib-')) { - // handle linked libs - $lib = substr($targetName, 4); - - return $text . ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.'; - } - - // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet - /*if ($providers = $pool->whatProvides($targetName, $this->reasonData->getConstraint(), true, true)) { - return $text . ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $providers) .' but these conflict with your requirements or minimum-stability.'; - }*/ - - return $text . ' -> no matching package found.'; + return $text . ' -> ' . $reason[1]; } return $text; @@ -249,7 +200,7 @@ abstract class Rule $learnedString = '(learned rule, '; if (isset($learnedPool[$this->reasonData])) { foreach ($learnedPool[$this->reasonData] as $learnedRule) { - $learnedString .= $learnedRule->getPrettyString($pool, $installedMap, $learnedPool); + $learnedString .= $learnedRule->getPrettyString($repositorySet, $request, $installedMap, $learnedPool); } } else { $learnedString .= 'reasoning unavailable'; @@ -272,20 +223,13 @@ abstract class Rule */ protected function formatPackagesUnique($pool, array $packages) { - // TODO this is essentially a duplicate of Problem: getPackageList, maintain in one place only? - $prepared = array(); - foreach ($packages as $package) { + foreach ($packages as $index => $package) { if (!is_object($package)) { - $package = $pool->literalToPackage($package); + $packages[$index] = $pool->literalToPackage($package); } - $prepared[$package->getName()]['name'] = $package->getPrettyName(); - $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); - } - foreach ($prepared as $name => $package) { - $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; } - return implode(', ', $prepared); + return Problem::getPackageList($packages); } } diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index 3db54d9df..9834e002f 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -12,6 +12,8 @@ namespace Composer\DependencyResolver; +use Composer\Repository\RepositorySet; + /** * @author Nils Adermann */ @@ -155,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable return array_keys($types); } - public function getPrettyString(Pool $pool = null) + public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null) { $string = "\n"; foreach ($this->rules as $type => $rules) { $string .= str_pad(self::$types[$type], 8, ' ') . ": "; foreach ($rules as $rule) { - $string .= ($pool ? $rule->getPrettyString($pool) : $rule)."\n"; + $string .= ($repositorySet && $request ? $rule->getPrettyString($repositorySet, $request) : $rule)."\n"; } $string .= "\n\n"; } @@ -171,6 +173,6 @@ class RuleSet implements \IteratorAggregate, \Countable public function __toString() { - return $this->getPrettyString(null); + return $this->getPrettyString(null, null); } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index be9494d91..d670c980a 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -29,7 +29,9 @@ class Solver /** @var PolicyInterface */ protected $policy; /** @var Pool */ - protected $pool = null; + protected $pool; + /** @var RepositorySet */ + protected $repositorySet; /** @var RuleSet */ protected $rules; @@ -65,11 +67,12 @@ class Solver * @param Pool $pool * @param IOInterface $io */ - public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io) + public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io, RepositorySet $repositorySet) { $this->io = $io; $this->policy = $policy; $this->pool = $pool; + $this->repositorySet = $repositorySet; } /** @@ -85,6 +88,11 @@ class Solver return $this->pool; } + public function getRepositorySet() + { + return $this->repositorySet; + } + // aka solver_makeruledecisions private function makeAssertionRuleDecisions() @@ -120,7 +128,7 @@ class Solver $conflict = $this->decisions->decisionRule($literal); if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) { - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule($rule); $problem->addRule($conflict); @@ -130,7 +138,7 @@ class Solver } // conflict with another root require/fixed package - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule($rule); $problem->addRule($conflict); @@ -177,7 +185,7 @@ class Solver } if (!$this->pool->whatProvides($packageName, $constraint)) { - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint))); $this->problems[] = $problem; } @@ -214,7 +222,7 @@ class Solver $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); if ($this->problems) { - throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool); + throw new SolverProblemsException($this->problems, $this->repositorySet, $request, $this->learnedPool); } return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); @@ -513,7 +521,7 @@ class Solver */ private function analyzeUnsolvable(Rule $conflictRule) { - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule($conflictRule); $this->analyzeUnsolvableRule($problem, $conflictRule); diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index f720aba4e..37768f436 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -13,6 +13,7 @@ namespace Composer\DependencyResolver; use Composer\Util\IniHelper; +use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -23,21 +24,21 @@ class SolverProblemsException extends \RuntimeException protected $installedMap; protected $learnedPool; - public function __construct(array $problems, array $installedMap, array $learnedPool) + public function __construct(array $problems, RepositorySet $repositorySet, Request $request, array $learnedPool) { $this->problems = $problems; - $this->installedMap = $installedMap; + $this->installedMap = $request->getPresentMap(true); $this->learnedPool = $learnedPool; - parent::__construct($this->createMessage(), 2); + parent::__construct($this->createMessage($repositorySet, $request), 2); } - protected function createMessage() + protected function createMessage(RepositorySet $repositorySet, Request $request) { $text = "\n"; $hasExtensionProblems = false; foreach ($this->problems as $i => $problem) { - $text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap, $this->learnedPool)."\n"; + $text .= " Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $this->installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index bbd81d997..542a9ee1c 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -389,7 +389,7 @@ class Installer $pool = $repositorySet->createPool($request); // solve dependencies - $solver = new Solver($policy, $pool, $this->io); + $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); $ruleSetSize = $solver->getRuleSetSize(); @@ -529,7 +529,7 @@ class Installer $pool = $repositorySet->createPool($request); //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); - $solver = new Solver($policy, $pool, $this->io); + $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); @@ -589,7 +589,7 @@ class Installer $pool = $repositorySet->createPool($request); // solve dependencies - $solver = new Solver($policy, $pool, $this->io); + $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); $solver = null; @@ -884,7 +884,7 @@ class Installer $packageQueue = new \SplQueue; $nameMatchesRequiredPackage = false; - $depPackages = $repositorySet->findPackages($packageName, null, false); + $depPackages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); $matchesByPattern = array(); // check if the name is a glob pattern that did not match directly @@ -892,7 +892,7 @@ class Installer // add any installed package matching the whitelisted name/pattern $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { - $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, false); + $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); } // add root requirements which match the whitelisted name/pattern @@ -933,7 +933,7 @@ class Installer $requires = $package->getRequires(); foreach ($requires as $require) { - $requirePackages = $repositorySet->findPackages($require->getTarget(), null, false); + $requirePackages = $repositorySet->findPackages($require->getTarget(), null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); foreach ($requirePackages as $requirePackage) { if (isset($this->updateWhitelist[$requirePackage->getName()])) { diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 03b872d47..517b27d7b 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -412,7 +412,7 @@ class PluginManager */ private function lookupInstalledPackage(RepositorySet $repositorySet, Link $link) { - $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint(), false); + $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint(), RepositorySet::ALLOW_PROVIDERS_REPLACERS | RepositorySet::ALLOW_SHADOWED_REPOSITORIES); return !empty($packages) ? $packages[0] : null; } diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index 6c7d0e65e..ab67d42d2 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -42,6 +42,11 @@ class ArrayRepository extends BaseRepository } } + public function getRepoName() + { + return 'array repo (defining '.count($this->packages).' package'.(count($this->packages) > 1 ? 's' : '').')'; + } + /** * {@inheritDoc} */ @@ -57,7 +62,9 @@ class ArrayRepository extends BaseRepository (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, $package->getNames(), $package->getStability()) ) { + // add selected packages which match stability requirements $result[spl_object_hash($package)] = $package; + // add the aliased package for packages where the alias matches if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } @@ -67,6 +74,7 @@ class ArrayRepository extends BaseRepository } } + // add aliases of packages that were selected, even if the aliases did not match foreach ($packages as $package) { if ($package instanceof AliasPackage) { if (isset($result[spl_object_hash($package->getAliasOf())])) { diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index aff80e4cd..d317b1404 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -43,6 +43,11 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito $this->repoConfig = $repoConfig; } + public function getRepoName() + { + return 'platform repo ('.$this->lookup.')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 3919d2a6b..dd1588a9a 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -125,6 +125,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->loop = new Loop($this->httpDownloader); } + public function getRepoName() + { + return 'composer repo ('.$this->url.')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index 8ead6693a..9565a9b86 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -39,6 +39,11 @@ class CompositeRepository extends BaseRepository } } + public function getRepoName() + { + return 'composite repo ('.count($this->repositories).' repos)'; + } + /** * Returns all the wrapped repositories * diff --git a/src/Composer/Repository/InstalledArrayRepository.php b/src/Composer/Repository/InstalledArrayRepository.php index 7ad05d0fa..de1dd67d8 100644 --- a/src/Composer/Repository/InstalledArrayRepository.php +++ b/src/Composer/Repository/InstalledArrayRepository.php @@ -21,4 +21,8 @@ namespace Composer\Repository; */ class InstalledArrayRepository extends WritableArrayRepository implements InstalledRepositoryInterface { + public function getRepoName() + { + return 'installed '.parent::getRepoName(); + } } diff --git a/src/Composer/Repository/InstalledFilesystemRepository.php b/src/Composer/Repository/InstalledFilesystemRepository.php index 1ff8a0a06..bf81734d4 100644 --- a/src/Composer/Repository/InstalledFilesystemRepository.php +++ b/src/Composer/Repository/InstalledFilesystemRepository.php @@ -19,4 +19,8 @@ namespace Composer\Repository; */ class InstalledFilesystemRepository extends FilesystemRepository implements InstalledRepositoryInterface { + public function getRepoName() + { + return 'installed '.parent::getRepoName(); + } } diff --git a/src/Composer/Repository/LockArrayRepository.php b/src/Composer/Repository/LockArrayRepository.php index 0ccc998d3..8da3d5915 100644 --- a/src/Composer/Repository/LockArrayRepository.php +++ b/src/Composer/Repository/LockArrayRepository.php @@ -21,5 +21,9 @@ namespace Composer\Repository; */ class LockArrayRepository extends ArrayRepository implements RepositoryInterface { + public function getRepoName() + { + return 'lock '.parent::getRepoName(); + } } diff --git a/src/Composer/Repository/PackageRepository.php b/src/Composer/Repository/PackageRepository.php index 52b9a0f6b..de6d31d4d 100644 --- a/src/Composer/Repository/PackageRepository.php +++ b/src/Composer/Repository/PackageRepository.php @@ -58,4 +58,9 @@ class PackageRepository extends ArrayRepository $this->addPackage($package); } } + + public function getRepoName() + { + return preg_replace('{^array }', 'package ', parent::getRepoName()); + } } diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 4db774579..4ae6d4b1c 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -111,6 +111,11 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn parent::__construct(); } + public function getRepoName() + { + return 'path repo ('.$this->repoConfig['url'].')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index 5cffb6233..97e131afb 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -67,6 +67,11 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn $this->repoConfig = $repoConfig; } + public function getRepoName() + { + return 'pear repo ('.$this->url.')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 897700707..360671c7e 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -51,6 +51,11 @@ class PlatformRepository extends ArrayRepository parent::__construct($packages); } + public function getRepoName() + { + return 'platform repo'; + } + protected function initialize() { parent::initialize(); @@ -275,7 +280,7 @@ class PlatformRepository extends ArrayRepository } else { $actualText = 'actual: '.$package->getPrettyVersion(); } - $overrider->setDescription($overrider->getDescription().' ('.$actualText.')'); + $overrider->setDescription($overrider->getDescription().', '.$actualText); return; } @@ -288,7 +293,7 @@ class PlatformRepository extends ArrayRepository } else { $actualText = 'actual: '.$package->getPrettyVersion(); } - $overrider->setDescription($overrider->getDescription().' ('.$actualText.')'); + $overrider->setDescription($overrider->getDescription().', '.$actualText); return; } diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 8b1bb3cff..9992778fb 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -83,4 +83,13 @@ interface RepositoryInterface extends \Countable * @return array[] an array of array('name' => '...', 'description' => '...') */ public function search($query, $mode = 0, $type = null); + + /** + * Returns a name representing this repository to the user + * + * This is best effort and definitely can not always be very precise + * + * @return string + */ + public function getRepoName(); } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 10f3b7dd6..21c7efe0e 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -29,6 +29,19 @@ use Composer\Package\Version\StabilityFilter; */ class RepositorySet { + /** + * Packages which replace/provide the given name might be returned as well even if they do not match the name exactly + */ + const ALLOW_PROVIDERS_REPLACERS = 1; + /** + * Packages are returned even though their stability does not match the required stability + */ + const ALLOW_UNACCEPTABLE_STABILITIES = 2; + /** + * Packages will be looked up in all repositories, even after they have been found in a higher prio one + */ + const ALLOW_SHADOWED_REPOSITORIES = 4; + /** @var array */ private $rootAliases; /** @var array */ @@ -39,7 +52,7 @@ class RepositorySet private $acceptableStabilities; private $stabilityFlags; - protected $rootRequires; + private $rootRequires; /** @var Pool */ private $pool; @@ -64,6 +77,11 @@ class RepositorySet } } + public function getRootRequires() + { + return $this->rootRequires; + } + /** * Adds a repository to this repository set * @@ -96,15 +114,32 @@ class RepositorySet * * @param string $name * @param ConstraintInterface|null $constraint - * @param bool $exactMatch if set to false, packages which replace/provide the given name might be returned as well even if they do not match the name exactly - * @param bool $ignoreStability if set to true, packages are returned even though their stability does not match the required stability + * @param int $flags any of the ALLOW_* constants from this class to tweak what is returned * @return array */ - public function findPackages($name, ConstraintInterface $constraint = null, $exactMatch = true, $ignoreStability = false) + public function findPackages($name, ConstraintInterface $constraint = null, $flags = 0) { + $exactMatch = ($flags & self::ALLOW_PROVIDERS_REPLACERS) === 0; + $ignoreStability = ($flags & self::ALLOW_UNACCEPTABLE_STABILITIES) !== 0; + $loadFromAllRepos = ($flags & self::ALLOW_SHADOWED_REPOSITORIES) !== 0; + $packages = array(); - foreach ($this->repositories as $repository) { - $packages[] = $repository->findPackages($name, $constraint) ?: array(); + if ($loadFromAllRepos) { + foreach ($this->repositories as $repository) { + $packages[] = $repository->findPackages($name, $constraint) ?: array(); + } + } else { + foreach ($this->repositories as $repository) { + $result = $repository->loadPackages(array($name => $constraint), $ignoreStability ? BasePackage::$stabilities : $this->acceptableStabilities, $ignoreStability ? array() : $this->stabilityFlags); + + $packages[] = $result['packages']; + foreach ($result['namesFound'] as $nameFound) { + // avoid loading the same package again from other repositories once it has been found + if ($name === $nameFound) { + break 2; + } + } + } } $candidates = $packages ? call_user_func_array('array_merge', $packages) : array(); @@ -135,7 +170,7 @@ class RepositorySet */ public function createPool(Request $request) { - $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $this->rootRequires); + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences); foreach ($this->repositories as $repo) { if ($repo instanceof InstalledRepositoryInterface) { diff --git a/src/Composer/Repository/RootPackageRepository.php b/src/Composer/Repository/RootPackageRepository.php index 8b5892717..721737fdc 100644 --- a/src/Composer/Repository/RootPackageRepository.php +++ b/src/Composer/Repository/RootPackageRepository.php @@ -21,4 +21,8 @@ namespace Composer\Repository; */ class RootPackageRepository extends ArrayRepository { + public function getRepoName() + { + return 'root package repo'; + } } diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 25b1b21b0..585b0e582 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -79,6 +79,17 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $this->processExecutor = new ProcessExecutor($io); } + public function getRepoName() + { + $driverClass = get_class($this->getDriver()); + $driverType = array_search($driverClass, $this->drivers); + if (!$driverType) { + $driverType = $driverClass; + } + + return 'vcs repo ('.$driverType.' '.$this->url.')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 2215c019b..3617e08fd 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -143,12 +143,18 @@ class RuleSetTest extends TestCase $p = $this->getPackage('foo', '2.1'), )); + $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); + $repositorySetMock->expects($this->any()) + ->method('getPool') + ->willReturn($pool); + $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); + $ruleSet = new RuleSet; $literal = $p->getId(); $rule = new GenericRule(array($literal), Rule::RULE_ROOT_REQUIRE, array('packageName' => 'foo/bar', 'constraint' => null)); $ruleSet->add($rule, RuleSet::TYPE_REQUEST); - $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($pool)); + $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($repositorySetMock, $requestMock)); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 6a01ff2f2..3ff6b765d 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -99,8 +99,14 @@ class RuleTest extends TestCase $p2 = $this->getPackage('baz', '1.1'), )); + $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); + $repositorySetMock->expects($this->any()) + ->method('getPool') + ->willReturn($pool); + $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); + $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo')); - $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($pool)); + $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index bc9b39204..bcbc47c97 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -82,7 +82,7 @@ class SolverTest extends TestCase $problems = $e->getProblems(); $this->assertCount(1, $problems); $this->assertEquals(2, $e->getCode()); - $this->assertEquals("\n - The requested package b could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString()); + $this->assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request)); } } @@ -682,13 +682,7 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $msg .= " - A 1.0 requires b >= 2.0 -> no matching package found.\n\n"; - $msg .= "Potential causes:\n"; - $msg .= " - A typo in the package name\n"; - $msg .= " - The package is not available in a stable-enough version according to your minimum-stability setting\n"; - $msg .= " see for more details.\n"; - $msg .= " - It's a private package and you forgot to add a custom repository to find it\n\n"; - $msg .= "Read for further common problems."; + $msg .= " - A 1.0 requires b >= 2.0 -> found B[1.0] but it does not match your constraint.\n"; $this->assertEquals($msg, $e->getMessage()); } } @@ -895,7 +889,7 @@ class SolverTest extends TestCase protected function createSolver() { - $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), new NullIO()); + $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), new NullIO(), $this->repoSet); } protected function checkSolverResult(array $expected) diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index d970d5c4c..c9a9dba6e 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -26,7 +26,7 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - c/c 1.0.0 requires x/x 1.0 -> no matching package found. + - c/c 1.0.0 requires x/x 1.0 -> could not be found in any version, there may be a typo in the package name. - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0]. - Root composer.json requires b/b 1.* -> satisfiable by b/b[1.0.0]. diff --git a/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test new file mode 100644 index 000000000..40c19f3eb --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test @@ -0,0 +1,38 @@ +--TEST-- +Test the error output of solver problems for conflicts between two dependents +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": "1.0.0"} }, + { "name": "victim/pkg", "version": "1.0.0" } + ] + } + ], + "require": { + "conflicter/pkg": "1.0.0", + "victim/pkg": "1.0.0" + } +} + + +--RUN-- +update + +--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 + - Root composer.json requires conflicter/pkg 1.0.0 -> satisfiable by conflicter/pkg[1.0.0]. + - victim/pkg 1.0.0 conflicts with conflicter/pkg[1.0.0]. + - Root composer.json requires victim/pkg 1.0.0 -> satisfiable by victim/pkg[1.0.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index 7d012cf7c..dcc94dff2 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -37,7 +37,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires a/a ~1.0 -> satisfiable by a/a[1.0.0]. - - a/a 1.0.0 requires php 5.5 -> your PHP version (%s) overridden by "config.platform.php" version (5.3) does not satisfy that requirement. + - a/a 1.0.0 requires php 5.5 -> your php version (5.3; overridden via config.platform, actual: %s) does not satisfy that requirement. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index 9735d0a2c..8e0b18e05 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -1,5 +1,5 @@ --TEST-- -Partial update from lock file should apply lock file and downgrade unstable packages even if not whitelisted +Partial update from lock file should apply lock file and if an unstable package is not allowed anymore by latest composer.json it should fail --COMPOSER-- { "repositories": [ @@ -59,12 +59,4 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - The requested package b/unstable could not be found in any version, there may be a typo in the package name. - -Potential causes: - - A typo in the package name - - The package is not available in a stable-enough version according to your minimum-stability setting - see for more details. - - It's a private package and you forgot to add a custom repository to find it - -Read for further common problems. + - b/unstable is fixed to 1.1.0-alpha (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you whitelist it for update. diff --git a/tests/Composer/Test/Fixtures/installer/repositories-priorities.test b/tests/Composer/Test/Fixtures/installer/repositories-priorities.test index 42f7a1a6e..bc06179e0 100644 --- a/tests/Composer/Test/Fixtures/installer/repositories-priorities.test +++ b/tests/Composer/Test/Fixtures/installer/repositories-priorities.test @@ -28,15 +28,8 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - The requested package foo/a could not be found in any version, there may be a typo in the package name. + - Root composer.json requires foo/a 2.*, it is satisfiable by foo/a[2.0.0] from package repo (defining 1 package) but foo/a[1.0.0] from package repo (defining 1 package) has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. -Potential causes: - - A typo in the package name - - The package is not available in a stable-enough version according to your minimum-stability setting - see for more details. - - It's a private package and you forgot to add a custom repository to find it - -Read for further common problems. --EXPECT-- --EXPECT-EXIT-CODE-- 2 diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index a0264257f..f3905b6fa 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -6,34 +6,84 @@ Test the error output of solver problems. { "type": "package", "package": [ + { "name": "package/found", "version": "2.0.0", "require": { + "unstable/package2": "2.*" + } }, + { "name": "package/found2", "version": "2.0.0", "require": { + "invalid/💩package": "*" + } }, + { "name": "package/found3", "version": "2.0.0", "require": { + "unstable/package2": "2.*" + } }, + { "name": "package/found4", "version": "2.0.0", "require": { + "non-existent/pkg2": "1.*" + } }, + { "name": "package/found5", "version": "2.0.0", "require": { + "requirer/pkg": "1.*" + } }, + { "name": "package/found6", "version": "2.0.0", "require": { + "stable-requiree-excluded/pkg2": "1.0.1" + } }, + { "name": "package/found7", "version": "2.0.0", "require": { + "php-64bit": "1.0.1" + } }, + { "name": "conflict/requirer", "version": "2.0.0", "require": { + "conflict/dep": "1.0.0" + } }, + { "name": "conflict/requirer2", "version": "2.0.0", "require": { + "conflict/dep": "2.0.0" + } }, + { "name": "conflict/dep", "version": "1.0.0" }, + { "name": "conflict/dep", "version": "2.0.0" }, { "name": "unstable/package", "version": "2.0.0-alpha" }, { "name": "unstable/package", "version": "1.0.0" }, - { "name": "requirer/pkg", "version": "1.0.0", "require": {"dependency/pkg": "1.0.0" } }, + { "name": "unstable/package2", "version": "2.0.0-alpha" }, + { "name": "unstable/package2", "version": "1.0.0" }, + { "name": "requirer/pkg", "version": "1.0.0", "require": { + "dependency/pkg": "1.0.0", + "dependency/unstable-pkg": "1.0.0-dev" + } }, { "name": "dependency/pkg", "version": "2.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "dependency/unstable-pkg", "version": "1.0.0-dev" }, { "name": "stable-requiree-excluded/pkg", "version": "1.0.1" }, { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } ] } ], "require": { + "package/found": "2.*", + "package/found2": "2.*", + "package/found3": "2.*", + "package/found4": "2.*", + "package/found5": "2.*", + "package/found6": "2.*", + "package/found7": "2.*", + "conflict/requirer": "2.*", + "conflict/requirer2": "2.*", "unstable/package": "2.*", - "bogus/pkg": "1.*", + "non-existent/pkg": "1.*", "requirer/pkg": "1.*", "dependency/pkg": "2.*", - "stable-requiree-excluded/pkg": "1.0.1" + "stable-requiree-excluded/pkg": "1.0.1", + "lib-xml": "1002.*", + "lib-icu": "1001.*", + "ext-xml": "1002.*", + "php": "1" } } --INSTALLED-- [ - { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }, + { "name": "stable-requiree-excluded/pkg2", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }, + { "name": "stable-requiree-excluded/pkg2", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -46,7 +96,7 @@ Test the error output of solver problems. } --RUN-- -update unstable/package requirer/pkg dependency/pkg +update unstable/package requirer/pkg dependency/pkg conflict/requirer --EXPECT-EXIT-CODE-- 2 @@ -57,14 +107,44 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - The requested package unstable/package could not be found in any version, there may be a typo in the package name. + - Root composer.json requires unstable/package 2.*, found unstable/package[2.0.0-alpha] but it does not match your minimum-stability. Problem 2 - - The requested package bogus/pkg could not be found in any version, there may be a typo in the package name. + - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. Problem 3 - - The requested package stable-requiree-excluded/pkg could not be found in any version, there may be a typo in the package name. + - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update. Problem 4 + - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. + Problem 5 + - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension. + Problem 6 + - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version (%s) installed. Install or enable PHP's xml extension. + Problem 7 + - Root composer.json requires php 1 but your php version (%s) does not satisfy that requirement. + Problem 8 + - Root composer.json requires package/found 2.* -> satisfiable by package/found[2.0.0]. + - package/found 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. + Problem 9 + - Root composer.json requires package/found2 2.* -> satisfiable by package/found2[2.0.0]. + - package/found2 2.0.0 requires invalid/💩package * -> could not be found, it looks like its name is invalid, "💩" is not allowed in package names. + Problem 10 + - Root composer.json requires package/found3 2.* -> satisfiable by package/found3[2.0.0]. + - package/found3 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. + Problem 11 + - Root composer.json requires package/found4 2.* -> satisfiable by package/found4[2.0.0]. + - package/found4 2.0.0 requires non-existent/pkg2 1.* -> could not be found in any version, there may be a typo in the package name. + Problem 12 + - Root composer.json requires package/found6 2.* -> satisfiable by package/found6[2.0.0]. + - package/found6 2.0.0 requires stable-requiree-excluded/pkg2 1.0.1 -> found stable-requiree-excluded/pkg2[1.0.0] but it does not match your constraint. + Problem 13 + - Root composer.json requires package/found7 2.* -> satisfiable by package/found7[2.0.0]. + - package/found7 2.0.0 requires php-64bit 1.0.1 -> your php-64bit version (%s) does not satisfy that requirement. + Problem 14 - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + Problem 15 + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + - package/found5 2.0.0 requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - Root composer.json requires package/found5 2.* -> satisfiable by package/found5[2.0.0]. Potential causes: - A typo in the package name @@ -73,6 +153,9 @@ Potential causes: - It's a private package and you forgot to add a custom repository to find it Read for further common problems. + To enable extensions, verify that they are enabled in your .ini files: +__inilist__ + You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode. --EXPECT-- diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index bb0643cf1..bb1473e71 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -323,6 +323,9 @@ class InstallerTest extends TestCase $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); if ($expectOutput) { + $output = preg_replace('{^ - .*?\.ini$}m', '__inilist__', $output); + $output = preg_replace('{(__inilist__\r?\n)+}', "__inilist__\n", $output); + $this->assertStringMatchesFormat(rtrim($expectOutput), rtrim($output)); } } From 29efc473a1e947638d8323cb5aa0419d4bdf3f6b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 09:23:20 +0100 Subject: [PATCH 211/321] Suggest which providers could be required to fulfill a virtual package requirement, fixes #2811 --- src/Composer/DependencyResolver/Problem.php | 11 +++++++++++ src/Composer/Repository/ComposerRepository.php | 17 +++++++++++++++++ src/Composer/Repository/RepositorySet.php | 13 +++++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index d91b48dd3..1070dc4f3 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -250,6 +250,17 @@ class Problem return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'); } + if ($providers = $repositorySet->getProviders($packageName)) { + $maxProviders = 20; + $providersStr = implode(array_map(function ($p) { + return " - ${p['name']} ".substr($p['description'], 0, 100)."\n"; + }, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers)); + if (count($providers) > $maxProviders+1) { + $providersStr .= ' ... and '.(count($providers)-$maxProviders).' more.'."\n"; + } + return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it: \n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement."); + } + return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name."); } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index dd1588a9a..a03974c9a 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -52,6 +52,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito protected $cache; protected $notifyUrl; protected $searchUrl; + /** @var string|null a URL containing %package% which can be queried to get providers of a given name */ + protected $providersApiUrl; protected $hasProviders = false; protected $providersUrl; protected $availablePackages; @@ -416,6 +418,17 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return parent::search($query, $mode); } + public function getProviders($packageName) + { + if (!$this->providersApiUrl) { + return array(); + } + + $result = $this->httpDownloader->get(str_replace('%package%', $packageName, $this->providersApiUrl), $this->options)->decodeJson(); + + return $result['providers']; + } + private function getProviderNames() { $this->loadRootServerFile(); @@ -810,6 +823,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasProviders = true; } + if (!empty($data['providers-api'])) { + $this->providersApiUrl = $data['providers-api']; + } + return $this->rootData = $data; } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 21c7efe0e..96fe94df9 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -158,6 +158,19 @@ class RepositorySet return $candidates; } + public function getProviders($packageName) + { + foreach ($this->repositories as $repository) { + if ($repository instanceof ComposerRepository) { + if ($providers = $repository->getProviders($packageName)) { + return $providers; + } + } + } + + return array(); + } + public function isPackageAcceptable($names, $stability) { return StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $names, $stability); From e6029d725af3ffe6effaee29acb88e4a9bf35e1f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 09:43:53 +0100 Subject: [PATCH 212/321] Add individual test checking for clashes between requirements at root and transitive dependency level --- .../conflict-between-root-and-dependent.test | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test diff --git a/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test b/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test new file mode 100644 index 000000000..8df58ef44 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test @@ -0,0 +1,40 @@ +--TEST-- +Test conflicts between a dependency's requirements and the root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "requirer/pkg", "version": "1.0.0", "require": { + "dependency/pkg": "1.0.0", + "dependency/unstable-pkg": "1.0.0-dev" + } }, + { "name": "dependency/pkg", "version": "2.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" } + ] + } + ], + "require": { + "requirer/pkg": "1.*", + "dependency/pkg": "2.*" + } +} + +--RUN-- +update + +--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 + - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + +--EXPECT-- + From 6c9d9e775ccf1d99b5b874bdf8e786d364963382 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 11:37:19 +0100 Subject: [PATCH 213/321] Clean up and deduplicate the output of RULE_LEARNED --- src/Composer/DependencyResolver/Rule.php | 12 ++--- .../installer/provider-conflicts3.test | 54 +++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/provider-conflicts3.test diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 462f3e4df..f92a38da3 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -196,18 +196,18 @@ abstract class Rule case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: - // TODO not sure this is a good idea, most of these rules should be listed in the problem anyway - $learnedString = '(learned rule, '; if (isset($learnedPool[$this->reasonData])) { + $learnedString = ', learned rules:'."\n - "; + $reasons = array(); foreach ($learnedPool[$this->reasonData] as $learnedRule) { - $learnedString .= $learnedRule->getPrettyString($repositorySet, $request, $installedMap, $learnedPool); + $reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $installedMap, $learnedPool); } + $learnedString .= implode("\n - ", array_unique($reasons)); } else { - $learnedString .= 'reasoning unavailable'; + $learnedString = ' (reasoning unavailable)'; } - $learnedString .= ')'; - return 'Conclusion: '.$ruleText.' '.$learnedString; + return 'Conclusion: '.$ruleText.$learnedString; case self::RULE_PACKAGE_ALIAS: return $ruleText; default: diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test new file mode 100644 index 000000000..7cc4f27b0 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -0,0 +1,54 @@ +--TEST-- +Test that a replacer can not be installed together with another version of the package it replaces +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + {"name": "replacer/pkg", "version": "2.0.0", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.1", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.2", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.3", "replace": { "regular/pkg": "self.version" }}, + {"name": "regular/pkg", "version": "1.0.0"}, + {"name": "regular/pkg", "version": "1.0.1"}, + {"name": "regular/pkg", "version": "1.0.2"}, + {"name": "regular/pkg", "version": "1.0.3"}, + {"name": "regular/pkg", "version": "2.0.0"}, + {"name": "regular/pkg", "version": "2.0.1"} + ] + } + ], + "require": { + "regular/pkg": "1.*", + "replacer/pkg": "2.*" + } +} + +--RUN-- +update + +--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 + - Conclusion: don't install regular/pkg 1.0.3, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. They all provide regular/pkg and can thus not coexist. + - Conclusion: don't install regular/pkg 1.0.2, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. They all provide regular/pkg and can thus not coexist. + - Conclusion: don't install regular/pkg 1.0.1, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. They all provide regular/pkg and can thus not coexist. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. They all provide regular/pkg and can thus not coexist. + - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3]. + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + +--EXPECT-- + From 1d4cdb60d0b4ac3041cd91ec8940bd378c018d3f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 12:27:24 +0100 Subject: [PATCH 214/321] Improve error reporting for conflicts/replaces of various kinds, fixes #7254 --- src/Composer/DependencyResolver/Rule.php | 43 +++++++++++++++- .../Test/DependencyResolver/SolverTest.php | 2 +- .../installer/provider-conflicts.test | 49 +++++++++++++++++++ .../installer/provider-conflicts2.test | 45 +++++++++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/provider-conflicts.test create mode 100644 tests/Composer/Test/Fixtures/installer/provider-conflicts2.test diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index f92a38da3..849268fed 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -188,11 +188,52 @@ abstract class Rule return $text; case self::RULE_PACKAGE_OBSOLETES: + if (count($literals) === 2 && $literals[0] < 0 && $literals[1] < 0) { + $package1 = $pool->literalToPackage($literals[0]); + $package2 = $pool->literalToPackage($literals[1]); + $conflictingNames = array_values(array_intersect($package1->getNames(), $package2->getNames())); + $provideClash = count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]; + + if ($conflictingNames && isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { + // swap vars so the if below passes + $tmp = $package2; + $package2 = $package1; + $package1 = $tmp; + } + if ($conflictingNames && !isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) { + return $package1->getPrettyString().' can not be installed as that would require removing '.$package2->getPrettyString().'. They both provide '.$provideClash.' and can thus not coexist.'; + } + if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { + if ($conflictingNames) { + return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'. They both provide '.$provideClash.' and can thus not coexist.'; + } + + return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'.'; + } + } + return $ruleText; case self::RULE_INSTALLED_PACKAGE_OBSOLETES: return $ruleText; case self::RULE_PACKAGE_SAME_NAME: - return 'Same name, can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.'; + $conflictingNames = null; + $allNames = array(); + foreach ($literals as $literal) { + $package = $pool->literalToPackage($literal); + if ($conflictingNames === null) { + $conflictingNames = $package->getNames(); + } else { + $conflictingNames = array_values(array_intersect($conflictingNames, $package->getNames())); + } + $allNames = array_unique(array_merge($allNames, $package->getNames())); + } + $provideClash = count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]; + + if ($conflictingNames && count($allNames) > 1) { + return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. They all provide '.$provideClash.' and can thus not coexist.'; + } + + return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.'; case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index bcbc47c97..74f0a480f 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -725,7 +725,7 @@ class SolverTest extends TestCase $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n"; $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n"; $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; - $msg .= " - Same name, can only install one of: B[0.9, 1.0].\n"; + $msg .= " - Only one of these can be installed: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $this->assertEquals($msg, $e->getMessage()); diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test new file mode 100644 index 000000000..4e9ce54fa --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test @@ -0,0 +1,49 @@ +--TEST-- +Test that providers provided by a dependent and root package cause a conflict +--COMPOSER-- +{ + "version": "1.2.3", + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "provider/pkg", + "version": "1.0.0", + "provide": { "root-provided/transitive-provided": "2.*", "root-replaced/transitive-provided": "2.*" }, + "replace": { "root-provided/transitive-replaced": "2.*", "root-replaced/transitive-replaced": "2.*" } + } + ] + } + ], + "require": { + "provider/pkg": "*" + }, + "provide": { + "root-provided/transitive-replaced": "2.*", + "root-provided/transitive-provided": "2.*" + }, + "replace": { + "root-replaced/transitive-replaced": "2.*", + "root-replaced/transitive-provided": "2.*" + } +} + +--RUN-- +update + +--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 + - __root__ is present at version 1.2.3 and cannot be modified by Composer + - provider/pkg 1.0.0 can not be installed as that would require removing __root__ 1.2.3. They both provide [root-provided/transitive-provided, root-replaced/transitive-provided, root-provided/transitive-replaced, root-replaced/transitive-replaced] and can thus not coexist. + - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test new file mode 100644 index 000000000..18f8f2db6 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test @@ -0,0 +1,45 @@ +--TEST-- +Test that providers provided by two dependents cause a conflict +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "provider/pkg", + "version": "1.0.0", + "provide": { "third/pkg": "2.*" } + }, + { + "name": "replacer/pkg", + "version": "1.0.0", + "replace": { "third/pkg": "2.*" } + } + ] + } + ], + "require": { + "provider/pkg": "*", + "replacer/pkg": "*" + } +} + +--RUN-- +update + +--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 + - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. + - Only one of these can be installed: replacer/pkg 1.0.0, provider/pkg 1.0.0. They both provide third/pkg and can thus not coexist. + - Root composer.json requires replacer/pkg * -> satisfiable by replacer/pkg[1.0.0]. + +--EXPECT-- + From f982a10447bd072086f149e4709a3467609cbe6d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 14:52:47 +0100 Subject: [PATCH 215/321] Update after rebase --- .../provider-packages-can-not-be-installed-unless-selected.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test b/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test index 85347b4e6..816b8efe9 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test +++ b/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test @@ -41,7 +41,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires foo/standard 1.0.0 -> satisfiable by foo/standard[1.0.0]. - - foo/standard 1.0.0 requires foo/does-not-exist 1.0.0 -> no matching package found. + - foo/standard 1.0.0 requires foo/does-not-exist 1.0.0 -> could not be found in any version, there may be a typo in the package name. Potential causes: - A typo in the package name From c41df325d837b898da4aaea14a9c9a09e1db2bb2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 15:23:22 +0100 Subject: [PATCH 216/321] Remove RepositorySet from Solver and remove getPool from RepositorySet --- src/Composer/DependencyResolver/Problem.php | 12 +++++------- src/Composer/DependencyResolver/Rule.php | 7 +++---- src/Composer/DependencyResolver/RuleSet.php | 6 +++--- src/Composer/DependencyResolver/Solver.php | 14 ++------------ .../SolverProblemsException.php | 11 +++++------ src/Composer/Installer.php | 6 +++--- src/Composer/Repository/RepositorySet.php | 19 ++++++------------- .../Test/DependencyResolver/RuleSetTest.php | 5 +---- .../Test/DependencyResolver/RuleTest.php | 5 +---- .../Test/DependencyResolver/SolverTest.php | 12 +++++++----- 10 files changed, 36 insertions(+), 61 deletions(-) diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 1070dc4f3..82e649e3f 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -63,7 +63,7 @@ class Problem * @param array $installedMap A map of all present packages * @return string */ - public function getPrettyString(RepositorySet $repositorySet, Request $request, array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) { // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections? $reasons = call_user_func_array('array_merge', array_reverse($this->reasons)); @@ -81,20 +81,20 @@ class Problem $constraint = $reasonData['constraint']; if (isset($constraint)) { - $packages = $repositorySet->getPool()->whatProvides($packageName, $constraint); + $packages = $pool->whatProvides($packageName, $constraint); } else { $packages = array(); } if (empty($packages)) { - return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $packageName, $constraint)); + return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint)); } } $messages = array(); foreach ($reasons as $rule) { - $messages[] = $rule->getPrettyString($repositorySet, $request, $installedMap, $learnedPool); + $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); } return "\n - ".implode("\n - ", $messages); @@ -125,10 +125,8 @@ class Problem /** * @internal */ - public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, $packageName, $constraint = null) + public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null) { - $pool = $repositorySet->getPool(); - // handle php/hhvm if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { $version = phpversion(); diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 849268fed..7dcdc5711 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -123,9 +123,8 @@ abstract class Rule abstract public function isAssertion(); - public function getPrettyString(RepositorySet $repositorySet, Request $request, array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) { - $pool = $repositorySet->getPool(); $literals = $this->getLiterals(); $ruleText = ''; @@ -180,7 +179,7 @@ abstract class Rule } else { $targetName = $this->reasonData->getTarget(); - $reason = Problem::getMissingPackageReason($repositorySet, $request, $targetName, $this->reasonData->getConstraint()); + $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint()); return $text . ' -> ' . $reason[1]; } @@ -241,7 +240,7 @@ abstract class Rule $learnedString = ', learned rules:'."\n - "; $reasons = array(); foreach ($learnedPool[$this->reasonData] as $learnedRule) { - $reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $installedMap, $learnedPool); + $reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); } $learnedString .= implode("\n - ", array_unique($reasons)); } else { diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index 9834e002f..d37ca1e9f 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -157,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable return array_keys($types); } - public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null) + public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null) { $string = "\n"; foreach ($this->rules as $type => $rules) { $string .= str_pad(self::$types[$type], 8, ' ') . ": "; foreach ($rules as $rule) { - $string .= ($repositorySet && $request ? $rule->getPrettyString($repositorySet, $request) : $rule)."\n"; + $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool) : $rule)."\n"; } $string .= "\n\n"; } @@ -173,6 +173,6 @@ class RuleSet implements \IteratorAggregate, \Countable public function __toString() { - return $this->getPrettyString(null, null); + return $this->getPrettyString(null, null, null); } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index d670c980a..e32fe2478 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -14,9 +14,7 @@ namespace Composer\DependencyResolver; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; -use Composer\Repository\RepositoryInterface; use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -30,8 +28,6 @@ class Solver protected $policy; /** @var Pool */ protected $pool; - /** @var RepositorySet */ - protected $repositorySet; /** @var RuleSet */ protected $rules; @@ -67,12 +63,11 @@ class Solver * @param Pool $pool * @param IOInterface $io */ - public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io, RepositorySet $repositorySet) + public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io) { $this->io = $io; $this->policy = $policy; $this->pool = $pool; - $this->repositorySet = $repositorySet; } /** @@ -88,11 +83,6 @@ class Solver return $this->pool; } - public function getRepositorySet() - { - return $this->repositorySet; - } - // aka solver_makeruledecisions private function makeAssertionRuleDecisions() @@ -222,7 +212,7 @@ class Solver $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); if ($this->problems) { - throw new SolverProblemsException($this->problems, $this->repositorySet, $request, $this->learnedPool); + throw new SolverProblemsException($this->problems, $this->learnedPool); } return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 37768f436..542fe0464 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -21,24 +21,23 @@ use Composer\Repository\RepositorySet; class SolverProblemsException extends \RuntimeException { protected $problems; - protected $installedMap; protected $learnedPool; - public function __construct(array $problems, RepositorySet $repositorySet, Request $request, array $learnedPool) + public function __construct(array $problems, array $learnedPool) { $this->problems = $problems; - $this->installedMap = $request->getPresentMap(true); $this->learnedPool = $learnedPool; - parent::__construct($this->createMessage($repositorySet, $request), 2); + parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2); } - protected function createMessage(RepositorySet $repositorySet, Request $request) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool) { + $installedMap = $request->getPresentMap(true); $text = "\n"; $hasExtensionProblems = false; foreach ($this->problems as $i => $problem) { - $text .= " Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $this->installedMap, $this->learnedPool)."\n"; + $text .= " Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 542a9ee1c..b1ed2db2c 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -396,7 +396,7 @@ class Installer $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); - $this->io->writeError($e->getMessage()); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); if (!$this->devMode) { $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); } @@ -536,7 +536,7 @@ class Installer $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); - $this->io->writeError($e->getMessage()); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); return max(1, $e->getCode()); } @@ -602,7 +602,7 @@ class Installer } } catch (SolverProblemsException $e) { $this->io->writeError('Your lock file does not contain a compatible set of packages. Please run composer update.', true, IOInterface::QUIET); - $this->io->writeError($e->getMessage()); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); return max(1, $e->getCode()); } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 96fe94df9..f931a17a2 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -54,8 +54,8 @@ class RepositorySet private $stabilityFlags; private $rootRequires; - /** @var Pool */ - private $pool; + /** @var bool */ + private $locked = false; public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $rootRequires = array()) { @@ -92,7 +92,7 @@ class RepositorySet */ public function addRepository(RepositoryInterface $repo) { - if ($this->pool) { + if ($this->locked) { throw new \RuntimeException("Pool has already been created from this repository set, it cannot be modified anymore."); } @@ -191,7 +191,9 @@ class RepositorySet } } - return $this->pool = $poolBuilder->buildPool($this->repositories, $request); + $this->locked = true; + + return $poolBuilder->buildPool($this->repositories, $request); } // TODO unify this with above in some simpler version without "request"? @@ -210,13 +212,4 @@ class RepositorySet return $this->createPool($request); } - - /** - * Access the pool object after it has been created, relevant for plugins which need to read info from the pool - * @return Pool - */ - public function getPool() - { - return $this->pool; - } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 3617e08fd..5a89ddf79 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -144,9 +144,6 @@ class RuleSetTest extends TestCase )); $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); - $repositorySetMock->expects($this->any()) - ->method('getPool') - ->willReturn($pool); $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); $ruleSet = new RuleSet; @@ -155,6 +152,6 @@ class RuleSetTest extends TestCase $ruleSet->add($rule, RuleSet::TYPE_REQUEST); - $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($repositorySetMock, $requestMock)); + $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($repositorySetMock, $requestMock, $pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 3ff6b765d..f819397fb 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -100,13 +100,10 @@ class RuleTest extends TestCase )); $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); - $repositorySetMock->expects($this->any()) - ->method('getPool') - ->willReturn($pool); $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo')); - $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock)); + $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 74f0a480f..e64002883 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -34,6 +34,7 @@ class SolverTest extends TestCase protected $request; protected $policy; protected $solver; + protected $pool; public function setUp() { @@ -82,7 +83,7 @@ class SolverTest extends TestCase $problems = $e->getProblems(); $this->assertCount(1, $problems); $this->assertEquals(2, $e->getCode()); - $this->assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request)); + $this->assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -653,7 +654,7 @@ class SolverTest extends TestCase $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $msg .= " - B 1.0 conflicts with A[1.0].\n"; $msg .= " - Root composer.json requires b -> satisfiable by B[1.0].\n"; - $this->assertEquals($msg, $e->getMessage()); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -683,7 +684,7 @@ class SolverTest extends TestCase $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $msg .= " - A 1.0 requires b >= 2.0 -> found B[1.0] but it does not match your constraint.\n"; - $this->assertEquals($msg, $e->getMessage()); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -728,7 +729,7 @@ class SolverTest extends TestCase $msg .= " - Only one of these can be installed: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $this->assertEquals($msg, $e->getMessage()); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -889,7 +890,8 @@ class SolverTest extends TestCase protected function createSolver() { - $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), new NullIO(), $this->repoSet); + $this->pool = $this->repoSet->createPool($this->request); + $this->solver = new Solver($this->policy, $this->pool, new NullIO()); } protected function checkSolverResult(array $expected) From 1e68555e0ae55c01300a9accc1bd5e4321522f57 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 15:50:46 +0100 Subject: [PATCH 217/321] Sanitize URLs in getRepoName and centralize the Url sanitization process --- src/Composer/Downloader/GitDownloader.php | 3 ++- .../Repository/ArtifactRepository.php | 2 +- .../Repository/ComposerRepository.php | 3 ++- .../Repository/CompositeRepository.php | 2 +- src/Composer/Repository/PathRepository.php | 3 ++- src/Composer/Repository/VcsRepository.php | 3 ++- src/Composer/Util/AuthHelper.php | 11 ---------- src/Composer/Util/Git.php | 15 ++----------- src/Composer/Util/Hg.php | 15 ++----------- src/Composer/Util/Http/CurlDownloader.php | 8 +++---- src/Composer/Util/RemoteFilesystem.php | 4 ++-- src/Composer/Util/Url.php | 17 +++++++++++++++ tests/Composer/Test/Util/UrlTest.php | 21 +++++++++++++++++++ 13 files changed, 58 insertions(+), 49 deletions(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index ba026d2e2..1d4ebe4b6 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -17,6 +17,7 @@ use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; use Composer\Util\Git as GitUtil; +use Composer\Util\Url; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Cache; @@ -434,7 +435,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $this->io->writeError(' '.$reference.' is gone (history was rewritten?)'); } - throw new \RuntimeException(GitUtil::sanitizeUrl('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput())); } protected function updateOriginUrl($path, $url) diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index d317b1404..a0acb7a61 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -45,7 +45,7 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito public function getRepoName() { - return 'platform repo ('.$this->lookup.')'; + return 'artifact repo ('.$this->lookup.')'; } public function getRepoConfig() diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index a03974c9a..0d5b67fa6 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -34,6 +34,7 @@ use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; use Composer\Util\Http\Response; use Composer\Util\MetadataMinifier; +use Composer\Util\Url; use React\Promise\Util as PromiseUtil; /** @@ -129,7 +130,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito public function getRepoName() { - return 'composer repo ('.$this->url.')'; + return 'composer repo ('.Url::sanitize($this->url).')'; } public function getRepoConfig() diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index 9565a9b86..806934b7d 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -41,7 +41,7 @@ class CompositeRepository extends BaseRepository public function getRepoName() { - return 'composite repo ('.count($this->repositories).' repos)'; + return 'composite repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->repositories)).')'; } /** diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 4ae6d4b1c..699270777 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -20,6 +20,7 @@ use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; +use Composer\Util\Url; /** * This repository allows installing local packages that are not necessarily under their own VCS. @@ -113,7 +114,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn public function getRepoName() { - return 'path repo ('.$this->repoConfig['url'].')'; + return 'path repo ('.Url::sanitize($this->repoConfig['url']).')'; } public function getRepoConfig() diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 585b0e582..5f4578d1e 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -22,6 +22,7 @@ use Composer\Package\Loader\LoaderInterface; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; +use Composer\Util\Url; use Composer\IO\IOInterface; use Composer\Config; @@ -87,7 +88,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $driverType = $driverClass; } - return 'vcs repo ('.$driverType.' '.$this->url.')'; + return 'vcs repo ('.$driverType.' '.Url::sanitize($this->url).')'; } public function getRepoConfig() diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index 7b8d33d68..e4e829ea7 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -255,15 +255,4 @@ class AuthHelper return count($pathParts) >= 4 && $pathParts[3] == 'downloads'; } - - /** - * @param string $url - * @return string - */ - public function stripCredentialsFromUrl($url) - { - // GitHub repository rename result in redirect locations containing the access_token as GET parameter - // e.g. https://api.github.com/repositories/9999999999?access_token=github_token - return preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url); - } } diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 798dd4a20..2239d33ad 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -362,27 +362,16 @@ class Git return '(' . implode('|', array_map('preg_quote', $config->get('gitlab-domains'))) . ')'; } - public static function sanitizeUrl($message) - { - return preg_replace_callback('{://(?P[^@]+?):(?P.+?)@}', function ($m) { - if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { - return '://***:***@'; - } - - return '://' . $m[1] . ':***@'; - }, $message); - } - private function throwException($message, $url) { // git might delete a directory when it fails and php will not know clearstatcache(); if (0 !== $this->process->execute('git --version', $ignoredOutput)) { - throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } - throw new \RuntimeException(self::sanitizeUrl($message)); + throw new \RuntimeException(Url::sanitize($message)); } /** diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php index 3681ad5c7..d0b7fe79f 100644 --- a/src/Composer/Util/Hg.php +++ b/src/Composer/Util/Hg.php @@ -72,23 +72,12 @@ class Hg $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url); } - public static function sanitizeUrl($message) - { - return preg_replace_callback('{://(?P[^@]+?):(?P.+?)@}', function ($m) { - if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { - return '://***:***@'; - } - - return '://' . $m[1] . ':***@'; - }, $message); - } - private function throwException($message, $url) { if (0 !== $this->process->execute('hg --version', $ignoredOutput)) { - throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } - throw new \RuntimeException(self::sanitizeUrl($message)); + throw new \RuntimeException(Url::sanitize($message)); } } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index ee93ca364..017b2d1a2 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -195,7 +195,7 @@ class CurlDownloader $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : ''; $ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : ''; if ($attributes['redirects'] === 0) { - $this->io->writeError('Downloading ' . $this->authHelper->stripCredentialsFromUrl($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); + $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); } $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); @@ -254,12 +254,12 @@ class CurlDownloader $contents = stream_get_contents($job['bodyHandle']); } $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); - $this->io->writeError('['.$statusCode.'] '.$this->authHelper->stripCredentialsFromUrl($progress['url']), true, IOInterface::DEBUG); + $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG); } else { rewind($job['bodyHandle']); $contents = stream_get_contents($job['bodyHandle']); $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); - $this->io->writeError('['.$statusCode.'] '.$this->authHelper->stripCredentialsFromUrl($progress['url']), true, IOInterface::DEBUG); + $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG); } fclose($job['bodyHandle']); @@ -362,7 +362,7 @@ class CurlDownloader } if (!empty($targetUrl)) { - $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, $this->authHelper->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, Url::sanitize($targetUrl)), true, IOInterface::DEBUG); return $targetUrl; } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index cf39aaf9d..6312deabc 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -246,7 +246,7 @@ class RemoteFilesystem $actualContextOptions = stream_context_get_options($ctx); $usingProxy = !empty($actualContextOptions['http']['proxy']) ? ' using proxy ' . $actualContextOptions['http']['proxy'] : ''; - $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $this->authHelper->stripCredentialsFromUrl($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); + $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); unset($origFileUrl, $actualContextOptions); // Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 @@ -704,7 +704,7 @@ class RemoteFilesystem $this->redirects++; $this->io->writeError('', true, IOInterface::DEBUG); - $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $this->authHelper->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, Url::sanitize($targetUrl)), true, IOInterface::DEBUG); $additionalOptions['redirects'] = $this->redirects; diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php index b12a2d54d..2da171556 100644 --- a/src/Composer/Util/Url.php +++ b/src/Composer/Util/Url.php @@ -102,4 +102,21 @@ class Url return $origin; } + + public static function sanitize($url) + { + // GitHub repository rename result in redirect locations containing the access_token as GET parameter + // e.g. https://api.github.com/repositories/9999999999?access_token=github_token + $url = preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url); + + $url = preg_replace_callback('{://(?P[^:/\s@]+):(?P[^@\s/]+)@}i', function ($m) { + if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) { + return '://***:***@'; + } + + return '://'.$m['user'].':***@'; + }, $url); + + return $url; + } } diff --git a/tests/Composer/Test/Util/UrlTest.php b/tests/Composer/Test/Util/UrlTest.php index 7772582a5..322bc0241 100644 --- a/tests/Composer/Test/Util/UrlTest.php +++ b/tests/Composer/Test/Util/UrlTest.php @@ -58,4 +58,25 @@ class UrlTest extends TestCase array('https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=abcd', 'https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=65', array('gitlab-domains' => array('mygitlab.com')), '65'), ); } + + /** + * @dataProvider sanitizeProvider + */ + public function testSanitize($expected, $url) + { + $this->assertSame($expected, Url::sanitize($url)); + } + + public static function sanitizeProvider() + { + return array( + array('https://foo:***@example.org/', 'https://foo:bar@example.org/'), + array('https://foo@example.org/', 'https://foo@example.org/'), + array('https://example.org/', 'https://example.org/'), + array('http://***:***@example.org', 'http://10a8f08e8d7b7b9:foo@example.org'), + array('https://foo:***@example.org:123/', 'https://foo:bar@example.org:123/'), + array('https://example.org/foo/bar?access_token=***', 'https://example.org/foo/bar?access_token=abcdef'), + array('https://example.org/foo/bar?foo=bar&access_token=***', 'https://example.org/foo/bar?foo=bar&access_token=abcdef'), + ); + } } From ec90c17e3b40b3c5c3d5d996f9ddacf9edecb852 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 15:51:56 +0100 Subject: [PATCH 218/321] Fix conflict order to be more accurate --- src/Composer/DependencyResolver/Rule.php | 2 +- tests/Composer/Test/DependencyResolver/SolverTest.php | 2 +- .../Test/Fixtures/installer/conflict-between-dependents.test | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 7dcdc5711..8f9dbc4ca 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -162,7 +162,7 @@ abstract class Rule $package1 = $pool->literalToPackage($literals[0]); $package2 = $pool->literalToPackage($literals[1]); - return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique($pool, array($package2)).'.'; + return $package2->getPrettyString().' conflicts with '.$package1->getPrettyString().'.'; case self::RULE_PACKAGE_REQUIRES: $sourceLiteral = array_shift($literals); diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index e64002883..c09ac07b1 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -652,7 +652,7 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $msg .= " - B 1.0 conflicts with A[1.0].\n"; + $msg .= " - A 1.0 conflicts with B 1.0.\n"; $msg .= " - Root composer.json requires b -> satisfiable by B[1.0].\n"; $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } diff --git a/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test index 40c19f3eb..a59b2ef98 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test @@ -31,7 +31,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires conflicter/pkg 1.0.0 -> satisfiable by conflicter/pkg[1.0.0]. - - victim/pkg 1.0.0 conflicts with conflicter/pkg[1.0.0]. + - conflicter/pkg 1.0.0 conflicts with victim/pkg 1.0.0. - Root composer.json requires victim/pkg 1.0.0 -> satisfiable by victim/pkg[1.0.0]. --EXPECT-- From 189d5adab0dd504d1c4eea2f6dde7f2d9c0302b2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 17:11:34 +0100 Subject: [PATCH 219/321] Fix reporting of replace conflicts to not mention provides --- src/Composer/DependencyResolver/Rule.php | 92 +++++++++++++------ .../Test/DependencyResolver/SolverTest.php | 2 +- .../installer/provider-conflicts.test | 4 +- .../installer/provider-conflicts2.test | 4 +- .../installer/provider-conflicts3.test | 8 +- 5 files changed, 74 insertions(+), 36 deletions(-) diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 8f9dbc4ca..58f6962cd 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -190,49 +190,77 @@ abstract class Rule if (count($literals) === 2 && $literals[0] < 0 && $literals[1] < 0) { $package1 = $pool->literalToPackage($literals[0]); $package2 = $pool->literalToPackage($literals[1]); - $conflictingNames = array_values(array_intersect($package1->getNames(), $package2->getNames())); - $provideClash = count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]; - if ($conflictingNames && isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { - // swap vars so the if below passes - $tmp = $package2; - $package2 = $package1; - $package1 = $tmp; + $replaces1 = $this->getReplacedNames($package1); + $replaces2 = $this->getReplacedNames($package2); + + $reason = null; + if ($conflictingNames = array_values(array_intersect($replaces1, $replaces2))) { + $reason = 'They both replace '.(count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]).' and can thus not coexist.'; + } elseif (in_array($package1->getName(), $replaces2, true)) { + $reason = $package2->getName().' replaces '.$package1->getName().' and can thus not coexist with it.'; + } elseif (in_array($package2->getName(), $replaces1, true)) { + $reason = $package1->getName().' replaces '.$package2->getName().' and can thus not coexist with it.'; } - if ($conflictingNames && !isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) { - return $package1->getPrettyString().' can not be installed as that would require removing '.$package2->getPrettyString().'. They both provide '.$provideClash.' and can thus not coexist.'; - } - if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { - if ($conflictingNames) { - return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'. They both provide '.$provideClash.' and can thus not coexist.'; + + if ($reason) { + if (isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { + // swap vars so the if below passes + $tmp = $package2; + $package2 = $package1; + $package1 = $tmp; + } + if (!isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) { + return $package1->getPrettyString().' can not be installed as that would require removing '.$package2->getPrettyString().'. '.$reason; } - return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'.'; + if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { + return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'. '.$reason; + } } + + return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'.'; } return $ruleText; case self::RULE_INSTALLED_PACKAGE_OBSOLETES: return $ruleText; case self::RULE_PACKAGE_SAME_NAME: - $conflictingNames = null; - $allNames = array(); + $replacedNames = null; + $packageNames = array(); foreach ($literals as $literal) { $package = $pool->literalToPackage($literal); - if ($conflictingNames === null) { - $conflictingNames = $package->getNames(); - } else { - $conflictingNames = array_values(array_intersect($conflictingNames, $package->getNames())); + $pkgReplaces = $this->getReplacedNames($package); + if ($pkgReplaces) { + if ($replacedNames === null) { + $replacedNames = $this->getReplacedNames($package); + } else { + $replacedNames = array_intersect($replacedNames, $this->getReplacedNames($package)); + } } - $allNames = array_unique(array_merge($allNames, $package->getNames())); - } - $provideClash = count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]; - - if ($conflictingNames && count($allNames) > 1) { - return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. They all provide '.$provideClash.' and can thus not coexist.'; + $packageNames[$package->getName()] = true; } - return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.'; + if ($replacedNames) { + $replacedNames = array_values(array_intersect(array_keys($packageNames), $replacedNames)); + } + if ($replacedNames && count($packageNames) > 1) { + $replacer = null; + foreach ($literals as $literal) { + $package = $pool->literalToPackage($literal); + if (array_intersect($replacedNames, $this->getReplacedNames($package))) { + $replacer = $package; + break; + } + } + $replacedNames = count($replacedNames) > 1 ? '['.implode(', ', $replacedNames).']' : $replacedNames[0]; + + if ($replacer) { + return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. '.$replacer->getName().' replaces '.$replacedNames.' and can thus not coexist with it.'; + } + } + + return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.'; case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: @@ -272,4 +300,14 @@ abstract class Rule return Problem::getPackageList($packages); } + + private function getReplacedNames(PackageInterface $package) + { + $names = array(); + foreach ($package->getReplaces() as $link) { + $names[] = $link->getTarget(); + } + + return $names; + } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index c09ac07b1..38e16c6a0 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -726,7 +726,7 @@ class SolverTest extends TestCase $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n"; $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n"; $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; - $msg .= " - Only one of these can be installed: B[0.9, 1.0].\n"; + $msg .= " - You can only install one version of a package, so only one of these can be installed: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test index 4e9ce54fa..1c8bd5608 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test @@ -1,5 +1,5 @@ --TEST-- -Test that providers provided by a dependent and root package cause a conflict +Test that names provided by a dependent and root package cause a conflict only for replace --COMPOSER-- { "version": "1.2.3", @@ -42,7 +42,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - __root__ is present at version 1.2.3 and cannot be modified by Composer - - provider/pkg 1.0.0 can not be installed as that would require removing __root__ 1.2.3. They both provide [root-provided/transitive-provided, root-replaced/transitive-provided, root-provided/transitive-replaced, root-replaced/transitive-replaced] and can thus not coexist. + - provider/pkg 1.0.0 can not be installed as that would require removing __root__ 1.2.3. They both replace root-replaced/transitive-replaced and can thus not coexist. - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test index 18f8f2db6..343dab537 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test @@ -1,5 +1,5 @@ --TEST-- -Test that providers provided by two dependents cause a conflict +Test that names provided by two dependents cause a conflict --COMPOSER-- { "repositories": [ @@ -38,7 +38,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. - - Only one of these can be installed: replacer/pkg 1.0.0, provider/pkg 1.0.0. They both provide third/pkg and can thus not coexist. + - Only one of these can be installed: replacer/pkg 1.0.0, provider/pkg 1.0.0. - Root composer.json requires replacer/pkg * -> satisfiable by replacer/pkg[1.0.0]. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test index 7cc4f27b0..3a865f3bf 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -39,14 +39,14 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Conclusion: don't install regular/pkg 1.0.3, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. They all provide regular/pkg and can thus not coexist. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. - Conclusion: don't install regular/pkg 1.0.2, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. They all provide regular/pkg and can thus not coexist. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. - Conclusion: don't install regular/pkg 1.0.1, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. They all provide regular/pkg and can thus not coexist. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. They all provide regular/pkg and can thus not coexist. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3]. - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. From 38f6ae2c4ee25406ac9165044dc71b183694399a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 17:30:38 +0100 Subject: [PATCH 220/321] Fix react/promise usage for v2 --- src/Composer/Repository/ComposerRepository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 0d5b67fa6..a0efb07a2 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -35,7 +35,7 @@ use Composer\Semver\Constraint\EmptyConstraint; use Composer\Util\Http\Response; use Composer\Util\MetadataMinifier; use Composer\Util\Url; -use React\Promise\Util as PromiseUtil; +use React\Promise\Promise; /** * @author Jordi Boggiano @@ -1112,7 +1112,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; if (isset($this->packagesNotFoundCache[$filename])) { - return PromiseUtil::promiseFor(array('packages' => array())); + return new Promise(function ($resolve, $reject) { $resolve(array('packages' => array())); }); } $httpDownloader = $this->httpDownloader; From 2f4ea3a463827e2293b3dc6106b6e1a5d27ae9ae Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 20:21:17 +0100 Subject: [PATCH 221/321] Fix show command, refactor RepositorySet constructor --- src/Composer/Command/CreateProjectCommand.php | 2 +- src/Composer/Command/InitCommand.php | 2 +- src/Composer/Command/ShowCommand.php | 5 +++-- src/Composer/Installer.php | 4 ++-- src/Composer/Plugin/PluginManager.php | 2 +- src/Composer/Repository/RepositorySet.php | 11 +++++++++-- .../Test/DependencyResolver/DefaultPolicyTest.php | 6 +++--- .../Test/DependencyResolver/PoolBuilderTest.php | 2 +- tests/Composer/Test/DependencyResolver/SolverTest.php | 2 +- 9 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 622e27787..315983499 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -311,7 +311,7 @@ EOT throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } - $repositorySet = new RepositorySet(array(), array(), $stability); + $repositorySet = new RepositorySet($stability); $repositorySet->addRepository($sourceRepo); $phpVersion = null; diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index e4c62ad69..d653ad61c 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -693,7 +693,7 @@ EOT $key = $minimumStability ?: 'default'; if (!isset($this->repositorySets[$key])) { - $this->repositorySets[$key] = $repositorySet = new RepositorySet(array(), array(), $minimumStability ?: $this->getMinimumStability($input)); + $this->repositorySets[$key] = $repositorySet = new RepositorySet($minimumStability ?: $this->getMinimumStability($input)); $repositorySet->addRepository($this->getRepos()); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 58cc969b3..3dd75c697 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -541,7 +541,8 @@ EOT $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version; $policy = new DefaultPolicy(); - $repositorySet = new RepositorySet(array(), array(), 'dev'); + $repositorySet = new RepositorySet('dev'); + $repositorySet->allowInstalledRepositories(); $repositorySet->addRepository($repos); $matchedPackage = null; @@ -1194,7 +1195,7 @@ EOT private function getRepositorySet(Composer $composer) { if (!$this->repositorySet) { - $this->repositorySet = new RepositorySet(array(), array(), $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); + $this->repositorySet = new RepositorySet($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index b1ed2db2c..f47d3ec09 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -719,7 +719,7 @@ class Installer $this->fixedRootPackage->setRequires(array()); $this->fixedRootPackage->setDevRequires(array()); - $repositorySet = new RepositorySet($rootAliases, $this->package->getReferences(), $minimumStability, $stabilityFlags, $rootRequires); + $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $rootAliases, $this->package->getReferences(), $rootRequires); $repositorySet->addRepository(new RootPackageRepository(array($this->fixedRootPackage))); $repositorySet->addRepository($platformRepo); if ($this->additionalFixedRepository) { @@ -873,7 +873,7 @@ class Installer } } - $repositorySet = new RepositorySet(array(), array(), 'dev'); + $repositorySet = new RepositorySet('dev'); $repositorySet->addRepository($lockRepo); $seen = array(); diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 517b27d7b..a706bf8ad 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -158,7 +158,7 @@ class PluginManager $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $repositorySet = new RepositorySet(array(), array(), 'dev'); + $repositorySet = new RepositorySet('dev'); $repositorySet->addRepository($localRepo); if ($globalRepo) { $repositorySet->addRepository($globalRepo); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index f931a17a2..80df9bb1d 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -56,8 +56,10 @@ class RepositorySet /** @var bool */ private $locked = false; + /** @var bool */ + private $allowInstalledRepositories = false; - public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $rootRequires = array()) + public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array()) { $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; @@ -77,6 +79,11 @@ class RepositorySet } } + public function allowInstalledRepositories($allow = true) + { + $this->allowInstalledRepositories = $allow; + } + public function getRootRequires() { return $this->rootRequires; @@ -186,7 +193,7 @@ class RepositorySet $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences); foreach ($this->repositories as $repo) { - if ($repo instanceof InstalledRepositoryInterface) { + if ($repo instanceof InstalledRepositoryInterface && !$this->allowInstalledRepositories) { throw new \LogicException('The pool can not accept packages from an installed repository'); } } diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index a2869290e..8a0e5a15a 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -36,7 +36,7 @@ class DefaultPolicyTest extends TestCase public function setUp() { - $this->repositorySet = new RepositorySet(array(), array(), 'dev'); + $this->repositorySet = new RepositorySet('dev'); $this->repo = new ArrayRepository; $this->repoLocked = new LockArrayRepository; @@ -144,7 +144,7 @@ class DefaultPolicyTest extends TestCase $this->assertSame($expected, $selected); - $this->repositorySet = new RepositorySet(array(), array(), 'dev'); + $this->repositorySet = new RepositorySet('dev'); $this->repositorySet->addRepository($repo2); $this->repositorySet->addRepository($repo1); @@ -250,7 +250,7 @@ class DefaultPolicyTest extends TestCase $repo->addPackage($packageA = clone $packageA); $repo->addPackage($packageB = clone $packageB); - $repositorySet = new RepositorySet(array(), array(), 'dev'); + $repositorySet = new RepositorySet('dev'); $repositorySet->addRepository($this->repo); $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer'), $this->repoLocked); diff --git a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php index 05bbe922d..c55431b6e 100644 --- a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php @@ -74,7 +74,7 @@ class PoolBuilderTest extends TestCase return $pkg; }; - $repositorySet = new RepositorySet($normalizedAliases, array(), $minimumStability, $stabilityFlags); + $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $normalizedAliases); $repositorySet->addRepository($repo = new ArrayRepository()); foreach ($packages as $package) { $repo->addPackage($loadPackage($package)); diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 38e16c6a0..46d882e9f 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -38,7 +38,7 @@ class SolverTest extends TestCase public function setUp() { - $this->repoSet = new RepositorySet(array()); + $this->repoSet = new RepositorySet(); $this->repo = new ArrayRepository; $this->repoLocked = new LockArrayRepository; From a1fe64152ddcb204b16fb648e43bee73e2cdf4a5 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 21:03:35 +0100 Subject: [PATCH 222/321] Minor Installer cleanups --- src/Composer/Installer.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index f47d3ec09..d9a1f87bd 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -345,7 +345,7 @@ class Installer // creating repository set $policy = $this->createPolicy(true); - $repositorySet = $this->createRepositorySet($platformRepo, $aliases); + $repositorySet = $this->createRepositorySet(true, $platformRepo, $aliases); $repositories = $this->repositoryManager->getRepositories(); foreach ($repositories as $repository) { $repositorySet->addRepository($repository); @@ -353,7 +353,6 @@ class Installer if ($lockedRepository) { $repositorySet->addRepository($lockedRepository); } - // TODO can we drop any locked packages that we have matching remote versions for? $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); @@ -377,7 +376,6 @@ class Installer if ($this->updateWhitelist && $lockedRepository) { foreach ($lockedRepository->getPackages() as $lockedPackage) { if (!$this->isUpdateable($lockedPackage)) { - // TODO add reason for fix? $request->fixPackage($lockedPackage); } } @@ -516,7 +514,7 @@ class Installer $resultRepo->addPackage($loader->load($dumper->dump($pkg))); } - $repositorySet = $this->createRepositorySet($platformRepo, $aliases, null); + $repositorySet = $this->createRepositorySet(true, $platformRepo, $aliases); $repositorySet->addRepository($resultRepo); $request = $this->createRequest($this->fixedRootPackage, $platformRepo, null); @@ -552,14 +550,12 @@ class Installer protected function doInstall(RepositoryInterface $localRepo, $alreadySolved = false) { $platformRepo = $this->createPlatformRepo(false); - $aliases = $this->getRootAliases(false); - $lockedRepository = $this->locker->getLockedRepository($this->devMode); // creating repository set $policy = $this->createPolicy(false); // use aliases from lock file only, so empty root aliases here - $repositorySet = $this->createRepositorySet($platformRepo, array(), $lockedRepository); + $repositorySet = $this->createRepositorySet(false, $platformRepo, array(), $lockedRepository); $repositorySet->addRepository($lockedRepository); $this->io->writeError('Installing dependencies from lock file'.($this->devMode ? ' (including require-dev)' : '').''); @@ -676,16 +672,15 @@ class Installer } /** + * @param bool $forUpdate + * @param PlatformRepository $platformRepo * @param array $rootAliases * @param RepositoryInterface|null $lockedRepository * @return RepositorySet */ - private function createRepositorySet(PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) + private function createRepositorySet($forUpdate, PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) { - // TODO what's the point of rootConstraints at all, we generate the package pool taking them into account anyway? - // TODO maybe we can drop the lockedRepository here - // TODO if this gets called in doInstall, this->update is still true?! - if ($this->update) { + if ($forUpdate) { $minimumStability = $this->package->getMinimumStability(); $stabilityFlags = $this->package->getStabilityFlags(); From 4f44b7b2213ca41e40e2ee34402dfe5871d69115 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 30 Jan 2020 22:49:15 +0100 Subject: [PATCH 223/321] Remove unused policy function findUpdatePackages --- src/Composer/DependencyResolver/DefaultPolicy.php | 13 ------------- src/Composer/DependencyResolver/PolicyInterface.php | 3 --- 2 files changed, 16 deletions(-) diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 0b978ec59..859410752 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -44,19 +44,6 @@ class DefaultPolicy implements PolicyInterface return $constraint->matchSpecific($version, true); } - public function findUpdatePackages(Pool $pool, PackageInterface $package, $mustMatchName = false) - { - $packages = array(); - - foreach ($pool->whatProvides($package->getName(), null, $mustMatchName) as $candidate) { - if ($candidate !== $package) { - $packages[] = $candidate; - } - } - - return $packages; - } - public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null) { $packages = $this->groupLiteralsByName($pool, $literals); diff --git a/src/Composer/DependencyResolver/PolicyInterface.php b/src/Composer/DependencyResolver/PolicyInterface.php index d4db7f3a9..49e23b041 100644 --- a/src/Composer/DependencyResolver/PolicyInterface.php +++ b/src/Composer/DependencyResolver/PolicyInterface.php @@ -20,8 +20,5 @@ use Composer\Package\PackageInterface; interface PolicyInterface { public function versionCompare(PackageInterface $a, PackageInterface $b, $operator); - - public function findUpdatePackages(Pool $pool, PackageInterface $package); - public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null); } From fdfdee03c1cb4af7e0cbbfe9b74b8a956c7c6d2f Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 30 Jan 2020 22:54:59 +0100 Subject: [PATCH 224/321] Remove unused argument to pool->match --- src/Composer/DependencyResolver/RuleSetGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 02afdfbf7..717a69d20 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -218,7 +218,7 @@ class RuleSetGenerator /** @var PackageInterface $possibleConflict */ foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) { - $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint(), true); + $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint()); if ($conflictMatch === Pool::MATCH || $conflictMatch === Pool::MATCH_REPLACE) { $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link)); From d14d411fa47b721e672f0d2644cf34db38ff24aa Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 31 Jan 2020 14:34:10 +0100 Subject: [PATCH 225/321] Add --dry-run to require and remove commands, fixes #7793 --- doc/03-cli.md | 2 ++ src/Composer/Command/RemoveCommand.php | 43 ++++++++++++++++++++++--- src/Composer/Command/RequireCommand.php | 26 +++++++++++++-- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index 5c4638b7c..68f7ba762 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -198,6 +198,7 @@ If you do not specify a package, composer will prompt you to search for a packag ### Options * **--dev:** Add packages to `require-dev`. +* **--dry-run:** Simulate the command without actually doing anything. * **--prefer-source:** Install packages from `source` when available. * **--prefer-dist:** Install packages from `dist` when available. * **--no-progress:** Removes the progress display that can mess with some @@ -236,6 +237,7 @@ uninstalled. ### Options * **--dev:** Remove packages from `require-dev`. +* **--dry-run:** Simulate the command without actually doing anything. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. * **--no-update:** Disables the automatic update of the dependencies. diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index f5562e01e..b95af9f72 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -38,6 +38,7 @@ class RemoveCommand extends BaseCommand ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), + new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), @@ -92,26 +93,44 @@ EOT } } + $dryRun = $input->getOption('dry-run'); + $toRemove = array(); foreach ($packages as $package) { if (isset($composer[$type][$package])) { - $json->removeLink($type, $composer[$type][$package]); + if ($dryRun) { + $toRemove[$type][] = $composer[$type][$package]; + } else { + $json->removeLink($type, $composer[$type][$package]); + } } elseif (isset($composer[$altType][$package])) { $io->writeError('' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . ''); if ($io->isInteractive()) { if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [yes]? ', true)) { - $json->removeLink($altType, $composer[$altType][$package]); + if ($dryRun) { + $toRemove[$altType][] = $composer[$altType][$package]; + } else { + $json->removeLink($altType, $composer[$altType][$package]); + } } } } elseif (isset($composer[$type]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) { foreach ($matches as $matchedPackage) { - $json->removeLink($type, $matchedPackage); + if ($dryRun) { + $toRemove[$type][] = $matchedPackage; + } else { + $json->removeLink($type, $matchedPackage); + } } } elseif (isset($composer[$altType]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) { foreach ($matches as $matchedPackage) { $io->writeError('' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . ''); if ($io->isInteractive()) { if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [yes]? ', true)) { - $json->removeLink($altType, $matchedPackage); + if ($dryRun) { + $toRemove[$altType][] = $matchedPackage; + } else { + $json->removeLink($altType, $matchedPackage); + } } } } @@ -128,6 +147,21 @@ EOT $this->resetComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins')); + if ($dryRun) { + $rootPackage = $composer->getPackage(); + $links = array( + 'require' => $rootPackage->getRequires(), + 'require-dev' => $rootPackage->getDevRequires(), + ); + foreach ($toRemove as $type => $packages) { + foreach ($packages as $package) { + unset($links[$type][$package]); + } + } + $rootPackage->setRequires($links['require']); + $rootPackage->setDevRequires($links['require-dev']); + } + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); @@ -149,6 +183,7 @@ EOT ->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setRunScripts(!$input->getOption('no-scripts')) + ->setDryRun($dryRun) ; $status = $install->run(); diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 10506170f..a881e7de1 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -21,6 +21,8 @@ use Composer\Installer; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Package\Version\VersionParser; +use Composer\Package\Loader\ArrayLoader; +use Composer\Package\BasePackage; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Repository\CompositeRepository; @@ -48,6 +50,7 @@ class RequireCommand extends InitCommand ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), + new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'), new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), @@ -195,7 +198,7 @@ EOT } } - if (!$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) { + if (!$input->getOption('dry-run') && !$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) { $composerDefinition = $this->json->read(); foreach ($requirements as $package => $version) { $composerDefinition[$requireKey][$package] = $version; @@ -211,19 +214,35 @@ EOT } try { - return $this->doUpdate($input, $output, $io, $requirements); + return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey); } catch (\Exception $e) { $this->revertComposerFile(false); throw $e; } } - private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements) + private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements, $requireKey, $removeKey) { // Update packages $this->resetComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins')); + if ($input->getOption('dry-run')) { + $rootPackage = $composer->getPackage(); + $links = array( + 'require' => $rootPackage->getRequires(), + 'require-dev' => $rootPackage->getDevRequires(), + ); + $loader = new ArrayLoader(); + $newLinks = $loader->parseLinks($rootPackage->getName(), $rootPackage->getPrettyVersion(), BasePackage::$supportedLinkTypes[$requireKey]['description'], $requirements); + $links[$requireKey] = array_merge($links[$requireKey], $newLinks); + foreach ($requirements as $package => $constraint) { + unset($links[$removeKey][$package]); + } + $rootPackage->setRequires($links['require']); + $rootPackage->setDevRequires($links['require-dev']); + } + $updateDevMode = !$input->getOption('update-no-dev'); $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative'); @@ -250,6 +269,7 @@ EOT ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) + ->setDryRun($input->getOption('dry-run')) ; // if no lock is present, or the file is brand new, we do not do a From 58e2956b954fc536a466096e96aa4303bfb95380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81ro=CC=82me=20Billiras?= Date: Fri, 6 Sep 2019 11:24:49 +0200 Subject: [PATCH 226/321] Allow referencing scripts to extends @composer --- src/Composer/EventDispatcher/EventDispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 5784c4c14..25bfb8f7c 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -173,7 +173,7 @@ class EventDispatcher $args = array_merge($script, $event->getArguments()); $flags = $event->getFlags(); if (substr($callable, 0, 10) === '@composer ') { - $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . substr($callable, 9); + $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . ' ' . implode(' ', $args); if (0 !== ($exitCode = $this->process->execute($exec))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); From f5726366286b9e2765dfe4947fa825701f306737 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 31 Jan 2020 15:45:33 +0100 Subject: [PATCH 227/321] Add support for TTY mode on Linux/OSX in script handlers when running in interactive mode, fixes #5856, fixes #3299, closes #4036 --- src/Composer/EventDispatcher/EventDispatcher.php | 4 ++-- src/Composer/Util/ProcessExecutor.php | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 25bfb8f7c..efd05d22c 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -174,7 +174,7 @@ class EventDispatcher $flags = $event->getFlags(); if (substr($callable, 0, 10) === '@composer ') { $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . ' ' . implode(' ', $args); - if (0 !== ($exitCode = $this->process->execute($exec))) { + if (0 !== ($exitCode = $this->process->execute($exec, $ignoredOutput, null, $this->io->isInteractive()))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode); @@ -248,7 +248,7 @@ class EventDispatcher } } - if (0 !== ($exitCode = $this->process->execute($exec))) { + if (0 !== ($exitCode = $this->process->execute($exec, $ignoredOutput, null, $this->io->isInteractive()))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode); diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index c16953652..905d012e7 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -41,7 +41,7 @@ class ProcessExecutor * @param string $cwd the working directory * @return int statuscode */ - public function execute($command, &$output = null, $cwd = null) + public function execute($command, &$output = null, $cwd = null, $tty = false) { if ($this->io && $this->io->isDebug()) { $safeCommand = preg_replace_callback('{://(?P[^:/\s]+):(?P[^@\s/]+)@}i', function ($m) { @@ -70,6 +70,9 @@ class ProcessExecutor } else { $process = new Process($command, $cwd, null, null, static::getTimeout()); } + if (!Platform::isWindows() && $tty) { + $process->setTty(true); + } $callback = is_callable($output) ? $output : array($this, 'outputHandler'); $process->run($callback); From 006c3de54289c3703ca7ae397f824c48bf9a8660 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 31 Jan 2020 16:33:34 +0100 Subject: [PATCH 228/321] Fix tests and make TTY usage on ProcessExecutor cleaner --- .../EventDispatcher/EventDispatcher.php | 13 ++++++-- src/Composer/Util/ProcessExecutor.php | 32 +++++++++++++++++-- .../EventDispatcher/EventDispatcherTest.php | 4 +++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index efd05d22c..b2ecdf023 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -174,7 +174,7 @@ class EventDispatcher $flags = $event->getFlags(); if (substr($callable, 0, 10) === '@composer ') { $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . ' ' . implode(' ', $args); - if (0 !== ($exitCode = $this->process->execute($exec, $ignoredOutput, null, $this->io->isInteractive()))) { + if (0 !== ($exitCode = $this->executeTty($exec))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode); @@ -248,7 +248,7 @@ class EventDispatcher } } - if (0 !== ($exitCode = $this->process->execute($exec, $ignoredOutput, null, $this->io->isInteractive()))) { + if (0 !== ($exitCode = $this->executeTty($exec))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode); @@ -265,6 +265,15 @@ class EventDispatcher return $return; } + protected function executeTty($exec) + { + if ($this->io->isInteractive()) { + return $this->process->executeTty($exec); + } + + return $this->process->execute($exec); + } + protected function getPhpExecCommand() { $finder = new PhpExecutableFinder(); diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 905d012e7..a30a04d15 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -15,6 +15,7 @@ namespace Composer\Util; use Composer\IO\IOInterface; use Symfony\Component\Process\Process; use Symfony\Component\Process\ProcessUtils; +use Symfony\Component\Process\Exception\RuntimeException; /** * @author Robert Schönthal @@ -41,7 +42,28 @@ class ProcessExecutor * @param string $cwd the working directory * @return int statuscode */ - public function execute($command, &$output = null, $cwd = null, $tty = false) + public function execute($command, &$output = null, $cwd = null) + { + if (func_num_args() > 1) { + return $this->doExecute($command, $cwd, false, $output); + } + + return $this->doExecute($command, $cwd, false); + } + + /** + * runs a process on the commandline in TTY mode + * + * @param string $command the command to execute + * @param string $cwd the working directory + * @return int statuscode + */ + public function executeTty($command, $cwd = null) + { + return $this->doExecute($command, $cwd, true); + } + + private function doExecute($command, $cwd, $tty, &$output = null) { if ($this->io && $this->io->isDebug()) { $safeCommand = preg_replace_callback('{://(?P[^:/\s]+):(?P[^@\s/]+)@}i', function ($m) { @@ -61,7 +83,7 @@ class ProcessExecutor $cwd = realpath(getcwd()); } - $this->captureOutput = func_num_args() > 1; + $this->captureOutput = func_num_args() > 3; $this->errorOutput = null; // TODO in v3, commands should be passed in as arrays of cmd + args @@ -71,7 +93,11 @@ class ProcessExecutor $process = new Process($command, $cwd, null, null, static::getTimeout()); } if (!Platform::isWindows() && $tty) { - $process->setTty(true); + try { + $process->setTty(true); + } catch (RuntimeException $e) { + // ignore TTY enabling errors + } } $callback = is_callable($output) ? $output : array($this, 'outputHandler'); diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 190d30f9d..1d6dcafe8 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -538,6 +538,10 @@ class EventDispatcherTest extends TestCase ->willReturn('> exit 1'); $io->expects($this->at(2)) + ->method('isInteractive') + ->willReturn(1); + + $io->expects($this->at(3)) ->method('writeError') ->with($this->equalTo('Script '.$code.' handling the post-install-cmd event returned with error code 1')); From 0b4763e6c721672014a603ca000165661cf98a4b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Feb 2020 13:06:20 +0100 Subject: [PATCH 229/321] Make it clear which package the error is for when detecting uncommitted changes --- src/Composer/Downloader/GitDownloader.php | 2 +- src/Composer/Downloader/SvnDownloader.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 1d4ebe4b6..483a3d364 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -301,7 +301,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $changes = array_map(function ($elem) { return ' '.$elem; }, preg_split('{\s*\r?\n\s*}', $changes)); - $this->io->writeError(' The package has modified files:'); + $this->io->writeError(' '.$package->getPrettyName().' has modified files:'); $this->io->writeError(array_slice($changes, 0, 10)); if (count($changes) > 10) { $this->io->writeError(' ' . (count($changes) - 10) . ' more files modified, choose "v" to view the full list'); diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index 47263c28a..b6ff41295 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -109,7 +109,7 @@ class SvnDownloader extends VcsDownloader return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose()); } catch (\RuntimeException $e) { throw new \RuntimeException( - 'Package could not be downloaded, '.$e->getMessage() + $package->getPrettyName().' could not be downloaded, '.$e->getMessage() ); } } @@ -135,7 +135,7 @@ class SvnDownloader extends VcsDownloader return ' '.$elem; }, preg_split('{\s*\r?\n\s*}', $changes)); $countChanges = count($changes); - $this->io->writeError(sprintf(' The package has modified file%s:', $countChanges === 1 ? '' : 's')); + $this->io->writeError(sprintf(' '.$package->getPrettyName().' has modified file%s:', $countChanges === 1 ? '' : 's')); $this->io->writeError(array_slice($changes, 0, 10)); if ($countChanges > 10) { $remainingChanges = $countChanges - 10; From 4e4304ae7df0992248c1ee3fea980c519424629a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 4 Feb 2020 13:22:46 +0100 Subject: [PATCH 230/321] Fix --- src/Composer/Downloader/SvnDownloader.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index b6ff41295..0281862b5 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -50,7 +50,7 @@ class SvnDownloader extends VcsDownloader } $this->io->writeError(" Checking out ".$package->getSourceReference()); - $this->execute($url, "svn co", sprintf("%s/%s", $url, $ref), null, $path); + $this->execute($package, $url, "svn co", sprintf("%s/%s", $url, $ref), null, $path); } /** @@ -72,7 +72,7 @@ class SvnDownloader extends VcsDownloader } $this->io->writeError(" Checking out " . $ref); - $this->execute($url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path); + $this->execute($target, $url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path); } /** @@ -101,7 +101,7 @@ class SvnDownloader extends VcsDownloader * @throws \RuntimeException * @return string */ - protected function execute($baseUrl, $command, $url, $cwd = null, $path = null) + protected function execute(PackageInterface $package, $baseUrl, $command, $url, $cwd = null, $path = null) { $util = new SvnUtil($baseUrl, $this->io, $this->config); $util->setCacheCredentials($this->cacheCredentials); From 7634520bdd7488c39c9ca9260c99067eed14eaf6 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 7 Feb 2020 22:35:57 +0100 Subject: [PATCH 231/321] Try to fix phpstan errors --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6c5993f0d..e81ff6523 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,7 @@ matrix: env: - deps=high - PHPSTAN=1 + - php: nightly fast_finish: true allow_failures: - php: nightly @@ -64,7 +65,7 @@ script: - ./vendor/bin/simple-phpunit # Run PHPStan - if [[ $PHPSTAN == "1" ]]; then - bin/composer require --dev phpstan/phpstan:^0.12 && + bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^6.5 && vendor/bin/phpstan analyse --configuration=phpstan/config.neon; fi From fbba17587402793760e391d2254611838f508f4f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 7 Feb 2020 22:49:17 +0100 Subject: [PATCH 232/321] Improve build some more --- .travis.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index e81ff6523..3aede8a71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,7 @@ matrix: env: - deps=high - PHPSTAN=1 + - SYMFONY_PHPUNIT_VERSION=7.5 - php: nightly fast_finish: true allow_failures: @@ -50,7 +51,7 @@ install: # flags to pass to install - flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-suggest --no-progress" # update deps to latest in case of high deps build - - if [ "$deps" == "high" ]; then composer config platform.php 7.2.4; composer update $flags; fi + - if [ "$deps" == "high" ]; then composer config platform.php 7.4.0; composer update $flags; fi # install dependencies using system provided composer binary - composer install $flags # install dependencies using composer from source @@ -62,11 +63,12 @@ before_script: - git config --global user.email travis@example.com script: - - ./vendor/bin/simple-phpunit - # Run PHPStan - if [[ $PHPSTAN == "1" ]]; then - bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^6.5 && + bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --no-update && + bin/composer update phpstan/* phpunit/* sebastian/* --with-dependencies && vendor/bin/phpstan analyse --configuration=phpstan/config.neon; + else + vendor/bin/simple-phpunit; fi before_deploy: From 589abb06a30ca49c2ab08fd66badcc0b9a2caad7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 7 Feb 2020 23:10:10 +0100 Subject: [PATCH 233/321] PHPStan fixes --- phpstan/config.neon | 6 ++++++ src/Composer/Downloader/VcsDownloader.php | 6 +++--- tests/Composer/Test/Console/HtmlOutputFormatterTest.php | 2 +- .../Test/Repository/Pear/PackageDependencyParserTest.php | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/phpstan/config.neon b/phpstan/config.neon index 78c45b90e..779fd3f8c 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -17,9 +17,15 @@ parameters: # variable defined in eval - '~^Undefined variable: \$res$~' + # erroneous detection of missing const, see https://github.com/phpstan/phpstan/issues/2960 + - '~^Access to undefined constant ZipArchive::OPSYS_UNIX.$~' + # we don't have different constructors for parent/child - '~^Unsafe usage of new static\(\)\.$~' + # BC with older PHPUnit + - '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~' + # hhvm should have support for $this in closures - count: 1 diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index ce0a4bd9f..4992bbe6a 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -68,7 +68,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa return $this->doDownload($package, $path, $url, $prevPackage); } catch (\Exception $e) { // rethrow phpunit exceptions to avoid hard to debug bug failures - if ($e instanceof \PHPUnit_Framework_Exception) { + if ($e instanceof \PHPUnit\Framework\Exception) { throw $e; } if ($this->io->isDebug()) { @@ -126,7 +126,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa break; } catch (\Exception $e) { // rethrow phpunit exceptions to avoid hard to debug bug failures - if ($e instanceof \PHPUnit_Framework_Exception) { + if ($e instanceof \PHPUnit\Framework\Exception) { throw $e; } if ($this->io->isDebug()) { @@ -179,7 +179,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa break; } catch (\Exception $exception) { // rethrow phpunit exceptions to avoid hard to debug bug failures - if ($exception instanceof \PHPUnit_Framework_Exception) { + if ($exception instanceof \PHPUnit\Framework\Exception) { throw $exception; } if ($this->io->isDebug()) { diff --git a/tests/Composer/Test/Console/HtmlOutputFormatterTest.php b/tests/Composer/Test/Console/HtmlOutputFormatterTest.php index d23bcddb2..a105eec0d 100644 --- a/tests/Composer/Test/Console/HtmlOutputFormatterTest.php +++ b/tests/Composer/Test/Console/HtmlOutputFormatterTest.php @@ -24,7 +24,7 @@ class HtmlOutputFormatterTest extends TestCase 'warning' => new OutputFormatterStyle('black', 'yellow'), )); - return $this->assertEquals( + $this->assertEquals( 'text green yellow black w/ yello bg', $formatter->format('text green yellow black w/ yello bg') ); diff --git a/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php b/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php index 0ca9259d9..f8cf3efca 100644 --- a/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php +++ b/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php @@ -53,7 +53,7 @@ class PackageDependencyParserTest extends TestCase { $data = json_decode(file_get_contents(__DIR__.'/Fixtures/DependencyParserTestData.json'), true); if (0 !== json_last_error()) { - throw new \PHPUnit_Framework_Exception('Invalid json file.'); + throw new \PHPUnit\Framework\Exception('Invalid json file.'); } return $data; From 21ab2f85bba2343041d5afdc2c95eff5dc376296 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 7 Feb 2020 23:21:48 +0100 Subject: [PATCH 234/321] Fix 7.4 build --- tests/Composer/Test/Command/RunScriptCommandTest.php | 3 ++- .../Test/Package/Archiver/ArchivableFilesFinderTest.php | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Command/RunScriptCommandTest.php b/tests/Composer/Test/Command/RunScriptCommandTest.php index 97acc6d26..6a2adbd2d 100644 --- a/tests/Composer/Test/Command/RunScriptCommandTest.php +++ b/tests/Composer/Test/Command/RunScriptCommandTest.php @@ -66,7 +66,8 @@ class RunScriptCommandTest extends TestCase $ed->expects($this->once()) ->method('dispatchScript') - ->with($scriptName, $expectedDevMode, array()); + ->with($scriptName, $expectedDevMode, array()) + ->willReturn(0); $composer = $this->createComposerInstance(); $composer->setEventDispatcher($ed); diff --git a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php index cea4088b1..b8f110937 100644 --- a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php @@ -309,7 +309,11 @@ class ArchivableFilesFinderTest extends TestCase protected function getArchivedFiles($command) { - $process = new Process($command, $this->sources); + if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { + $process = Process::fromShellCommandline($command, $this->sources); + } else { + $process = new Process($command, $this->sources); + } $process->run(); $archive = new \PharData($this->sources.'/archive.zip'); From 54d57fc6e48c3e954b8655211ed6c16c760af6f4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 7 Feb 2020 23:23:48 +0100 Subject: [PATCH 235/321] Make sure we test locked dependencies with 7.4 too --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3aede8a71..47dec258e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,10 +25,16 @@ matrix: - php: 7.1 - php: 7.2 - php: 7.3 + # Regular 7.4 build with locked deps + - php: 7.4 + env: + - SYMFONY_PHPUNIT_VERSION=7.5 + # High deps check - php: 7.4 env: - deps=high - SYMFONY_PHPUNIT_VERSION=7.5 + # PHPStan checks - php: 7.4 env: - deps=high From 702a532a61124aa5b30580d2847d2eaf574eb130 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 10 Feb 2020 08:39:49 +0100 Subject: [PATCH 236/321] Build improvements --- .travis.yml | 14 ++++++++------ phpstan/config.neon | 3 --- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 47dec258e..00e227954 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: php -dist: trusty +dist: bionic git: depth: 5 @@ -9,22 +9,24 @@ cache: directories: - $HOME/.composer/cache -addons: - apt: - packages: - - parallel - matrix: include: - php: 5.3 dist: precise - php: 5.4 + dist: trusty - php: 5.5 + dist: trusty - php: 5.6 + dist: xenial - php: 7.0 + dist: xenial - php: 7.1 + dist: xenial - php: 7.2 + dist: xenial - php: 7.3 + dist: xenial # Regular 7.4 build with locked deps - php: 7.4 env: diff --git a/phpstan/config.neon b/phpstan/config.neon index 779fd3f8c..48b15acb1 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -17,9 +17,6 @@ parameters: # variable defined in eval - '~^Undefined variable: \$res$~' - # erroneous detection of missing const, see https://github.com/phpstan/phpstan/issues/2960 - - '~^Access to undefined constant ZipArchive::OPSYS_UNIX.$~' - # we don't have different constructors for parent/child - '~^Unsafe usage of new static\(\)\.$~' From ef249ef6b63310fe1d6ea2bdb002a6c50e64021b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 11 Feb 2020 11:01:20 +0100 Subject: [PATCH 237/321] Add support for lib-zip platform package --- src/Composer/Repository/PlatformRepository.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 360671c7e..84f3d4b66 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -221,6 +221,13 @@ class PlatformRepository extends ArrayRepository $prettyVersion = LIBXSLT_DOTTED_VERSION; break; + case 'zip': + if (defined('ZipArchive::LIBZIP_VERSION')) { + $prettyVersion = \ZipArchive::LIBZIP_VERSION; + } else { + continue 2; + } + default: // None handled extensions have no special cases, skip continue 2; From fc5682ed9f8a8c849ce6df9a8592616e2395f061 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 11 Feb 2020 11:41:31 +0100 Subject: [PATCH 238/321] Fix build --- tests/Composer/Test/Command/ArchiveCommandTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Command/ArchiveCommandTest.php b/tests/Composer/Test/Command/ArchiveCommandTest.php index 4a777988d..41a5c0993 100644 --- a/tests/Composer/Test/Command/ArchiveCommandTest.php +++ b/tests/Composer/Test/Command/ArchiveCommandTest.php @@ -95,9 +95,9 @@ class ArchiveCommandTest extends TestCase null, false, null - ); + )->willReturn(0); $command->method('isInteractive')->willReturn(false); - $command->run($input, $output); + $this->assertEquals(0, $command->run($input, $output)); } } From 12a63755483e95870182110aedce932f90d5dd31 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 11 Feb 2020 11:46:19 +0100 Subject: [PATCH 239/321] Ignore phpstan error --- phpstan/config.neon | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpstan/config.neon b/phpstan/config.neon index 48b15acb1..2b26f4709 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -17,6 +17,9 @@ parameters: # variable defined in eval - '~^Undefined variable: \$res$~' + # erroneous detection of missing const, see https://github.com/phpstan/phpstan/issues/2960 + - '~^Access to undefined constant ZipArchive::LIBZIP_VERSION.$~' + # we don't have different constructors for parent/child - '~^Unsafe usage of new static\(\)\.$~' From 9609729de7ce39e206161e6512cf69de496d0107 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 11 Feb 2020 13:43:38 +0100 Subject: [PATCH 240/321] Change update to upgrade for package update operations which are upgrades, refs #8594 --- doc/01-basic-usage.md | 2 +- src/Composer/Command/SelfUpdateCommand.php | 2 +- .../DependencyResolver/Operation/UpdateOperation.php | 2 +- .../Test/Fixtures/installer/github-issues-4795-2.test | 8 ++++---- .../installer/install-from-lock-removes-package.test | 2 +- ...-dev-reference-from-lock-for-non-updated-packages.test | 4 ++-- .../Test/Fixtures/installer/partial-update-from-lock.test | 2 +- .../partial-update-installs-from-lock-even-missing.test | 4 ++-- .../root-requirements-do-not-affect-locked-versions.test | 2 +- .../Test/Fixtures/installer/update-alias-lock.test | 2 +- tests/Composer/Test/Fixtures/installer/update-alias.test | 2 +- .../Test/Fixtures/installer/update-all-dry-run.test | 4 ++-- tests/Composer/Test/Fixtures/installer/update-all.test | 4 ++-- .../Test/Fixtures/installer/update-changes-url.test | 6 +++--- .../Fixtures/installer/update-dev-ignores-providers.test | 2 +- .../installer/update-dev-packages-updates-repo-url.test | 2 +- .../installer/update-dev-to-new-ref-picks-up-changes.test | 2 +- .../update-ignore-platform-package-requirements.test | 2 +- .../Fixtures/installer/update-installed-reference.test | 2 +- .../installer/update-no-dev-still-resolves-dev.test | 4 ++-- .../installer/update-picks-up-change-of-vcs-type.test | 2 +- .../Fixtures/installer/update-prefer-lowest-stable.test | 2 +- .../Fixtures/installer/update-reference-picks-latest.test | 2 +- .../Test/Fixtures/installer/update-reference.test | 2 +- .../installer/update-whitelist-locked-require.test | 4 ++-- .../update-whitelist-patterns-with-all-dependencies.test | 6 +++--- .../update-whitelist-patterns-with-dependencies.test | 6 +++--- .../update-whitelist-patterns-with-root-dependencies.test | 6 +++--- .../update-whitelist-patterns-without-dependencies.test | 4 ++-- .../Fixtures/installer/update-whitelist-patterns.test | 8 ++++---- .../Fixtures/installer/update-whitelist-reads-lock.test | 4 ++-- .../installer/update-whitelist-removes-unused.test | 2 +- .../installer/update-whitelist-with-dependencies.test | 4 ++-- .../Test/Fixtures/installer/update-whitelist.test | 2 +- .../Fixtures/installer/update-with-all-dependencies.test | 8 ++++---- .../updating-dev-from-lock-removes-old-deps.test | 2 +- .../installer/updating-dev-updates-url-and-reference.test | 2 +- 37 files changed, 63 insertions(+), 63 deletions(-) diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index e1faa6318..de511260d 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -159,7 +159,7 @@ php composer.phar update > if the `composer.lock` has not been updated since changes were made to the > `composer.json` that might affect dependency resolution. -If you only want to install or update one dependency, you can whitelist them: +If you only want to install, upgrade or remove one dependency, you can whitelist them: ```sh php composer.phar update monolog/monolog [...] diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 9beb0b8c4..356523492 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -154,7 +154,7 @@ EOT $updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion); - $io->write(sprintf("Updating to version %s (%s channel).", $updateVersion, $versionsUtil->getChannel())); + $io->write(sprintf("Upgrading to version %s (%s channel).", $updateVersion, $versionsUtil->getChannel())); $remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar'); $signature = $httpDownloader->get($remoteFilename.'.sig')->getBody(); $io->writeError(' ', false); diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index 38ca6e330..61bcd7f5b 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -86,7 +86,7 @@ class UpdateOperation extends SolverOperation $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); } - $actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Updating' : 'Downgrading'; + $actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading'; return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')'; } diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index 454215ee0..b8968c35b 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -55,13 +55,13 @@ update a/a b/b --with-dependencies Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 2 updates, 0 removals - - Updating a/a (1.0.0 => 1.1.0) - - Updating b/b (1.0.0 => 1.1.0) + - Upgrading a/a (1.0.0 => 1.1.0) + - Upgrading b/b (1.0.0 => 1.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- -Updating a/a (1.0.0 => 1.1.0) -Updating b/b (1.0.0 => 1.1.0) +Upgrading a/a (1.0.0 => 1.1.0) +Upgrading b/b (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index 7e7f0dfb5..d60cd7e94 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -41,4 +41,4 @@ Install from a lock file that deleted a package install --EXPECT-- Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0 => 1.1.0) +Upgrading whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test index d3da18878..f45f6d528 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test @@ -93,5 +93,5 @@ update b/b "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster-a => dev-master newmaster-a) -Updating b/b (dev-master oldmaster-b => dev-master newmaster-b2) +Upgrading a/a (dev-master oldmaster-a => dev-master newmaster-a) +Upgrading b/b (dev-master oldmaster-b => dev-master newmaster-b2) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test index 32018105d..bce29a21f 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -74,7 +74,7 @@ update b/unstable "platform-dev": [] } --EXPECT-- -Updating a/old (0.9.0 => 1.0.0) +Upgrading a/old (0.9.0 => 1.0.0) Downgrading b/unstable (1.1.0-alpha => 1.0.0) Downgrading c/uptodate (2.0.0 => 1.0.0) Installing d/removed (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test index b21905e07..7530ca862 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test @@ -97,9 +97,9 @@ update b/b "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster-a => dev-master newmaster-a) +Upgrading a/a (dev-master oldmaster-a => dev-master newmaster-a) Marking a/a (2.2.x-dev newmaster-a) as installed, alias of a/a (dev-master newmaster-a) -Updating b/b (dev-master oldmaster-b => dev-master newmaster-b2) +Upgrading b/b (dev-master oldmaster-b => dev-master newmaster-b2) Marking b/b (2.3.x-dev newmaster-b2) as installed, alias of b/b (dev-master newmaster-b2) Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a) Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) diff --git a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test index 1895992f6..202767c1f 100644 --- a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test +++ b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test @@ -38,4 +38,4 @@ The locked version will not get overwritten by an install --RUN-- install --EXPECT-- -Updating foo/baz (1.0.0 => 2.0.0) +Upgrading foo/baz (1.0.0 => 2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index 1018fc678..5aceda8ea 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -59,4 +59,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master 1234 => dev-master master) +Upgrading a/a (dev-master 1234 => dev-master master) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias.test b/tests/Composer/Test/Fixtures/installer/update-alias.test index 6a7e434c8..8da3d4d23 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias.test @@ -33,5 +33,5 @@ Update aliased package to non-aliased version --RUN-- update --EXPECT-- -Updating a/a (dev-master master => dev-foo foo) +Upgrading a/a (dev-master master => dev-foo foo) Marking a/a (1.0.x-dev master) as uninstalled, alias of a/a (dev-master master) diff --git a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test index f3484d93b..3d93d23bd 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test +++ b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test @@ -44,6 +44,6 @@ Lock file operations: 3 installs, 0 updates, 0 removals - Locking a/c (1.0.0) Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals - - Updating a/a (1.0.0 => 1.0.1) - - Updating a/b (1.0.0 => 2.0.0) + - Upgrading a/a (1.0.0 => 1.0.1) + - Upgrading a/b (1.0.0 => 2.0.0) --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-all.test b/tests/Composer/Test/Fixtures/installer/update-all.test index 9a5fe8e9d..893f9717c 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all.test +++ b/tests/Composer/Test/Fixtures/installer/update-all.test @@ -36,5 +36,5 @@ Updates updateable packages --RUN-- update --EXPECT-- -Updating a/a (1.0.0 => 1.0.1) -Updating a/b (1.0.0 => 2.0.0) +Upgrading a/a (1.0.0 => 1.0.1) +Upgrading a/b (1.0.0 => 2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index e9c703f41..8f6715769 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -214,7 +214,7 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an --RUN-- update a/a b/b d/d g/g --EXPECT-- -Updating a/a (dev-master 1111111 => dev-master 2222222) -Updating b/b (2.0.3 1111111 => 2.0.3 2222222) +Upgrading a/a (dev-master 1111111 => dev-master 2222222) +Upgrading b/b (2.0.3 1111111 => 2.0.3 2222222) Installing e/e (dev-master 1111111) -Updating g/g (dev-master 0000000 => dev-master 1111111) +Upgrading g/g (dev-master 0000000 => dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test b/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test index a1ae85a88..68c02006d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-ignores-providers.test @@ -35,4 +35,4 @@ Updating a dev package selects its newest version but no providers --RUN-- update --EXPECT-- -Updating a/installed (dev-master oldref => dev-master newref) +Upgrading a/installed (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test b/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test index 7faef167c..0a918b183 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test @@ -93,4 +93,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldmaster => dev-master newmaster) +Upgrading a/a (dev-master oldmaster => dev-master newmaster) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test b/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test index c84385e64..12ad75586 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test @@ -38,4 +38,4 @@ Updating a dev package to its latest ref should pick up new dependencies update --EXPECT-- Installing a/dependency (dev-master ref) -Updating a/devpackage (dev-master oldref => dev-master newref) +Upgrading a/devpackage (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test index b12dae58b..30e0e6112 100644 --- a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test +++ b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test @@ -23,4 +23,4 @@ Update in ignore-platform-reqs mode --RUN-- update --ignore-platform-reqs --EXPECT-- -Updating a/a (1.0.0 => 1.0.1) +Upgrading a/a (1.0.0 => 1.0.1) diff --git a/tests/Composer/Test/Fixtures/installer/update-installed-reference.test b/tests/Composer/Test/Fixtures/installer/update-installed-reference.test index 7c8df0459..ae0b2d537 100644 --- a/tests/Composer/Test/Fixtures/installer/update-installed-reference.test +++ b/tests/Composer/Test/Fixtures/installer/update-installed-reference.test @@ -28,4 +28,4 @@ Updating a dev package forcing it's reference should not do anything if the refe --RUN-- update --EXPECT-- -Updating a/a (dev-master def000 => dev-master ) +Upgrading a/a (dev-master def000 => dev-master ) diff --git a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test index 17d16df6d..f0c8282a1 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test @@ -61,8 +61,8 @@ Updates with --no-dev but we still end up with a complete lock file including de update --no-dev --EXPECT-- Uninstalling a/b (1.0.0) -Updating a/a (1.0.0 => 1.0.1) +Upgrading a/a (1.0.0 => 1.0.1) Installing a/c (1.0.0) -Updating dev/pkg (dev-master old => dev-master new) +Upgrading dev/pkg (dev-master old => dev-master new) Marking dev/pkg (1.1.x-dev new) as installed, alias of dev/pkg (dev-master new) Marking dev/pkg (1.0.x-dev old) as uninstalled, alias of dev/pkg (dev-master old) diff --git a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test index 18213d3b9..c1e746356 100644 --- a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test +++ b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test @@ -62,4 +62,4 @@ update mirrors "platform-dev": [] } --EXPECT-- -Updating a/a (1.0.0 old-hg-ref => 1.0.0 new-git-ref) +Upgrading a/a (1.0.0 old-hg-ref => 1.0.0 new-git-ref) diff --git a/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test index 8cf5918e9..58935c8d7 100644 --- a/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test +++ b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test @@ -36,5 +36,5 @@ Updates packages to their lowest stable version --RUN-- update --prefer-lowest --prefer-stable --EXPECT-- -Updating a/a (1.0.0-rc1 => 1.0.1) +Upgrading a/a (1.0.0-rc1 => 1.0.1) Downgrading a/b (1.0.1 => 1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test b/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test index 6b1f540bb..734315eb5 100644 --- a/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test +++ b/tests/Composer/Test/Fixtures/installer/update-reference-picks-latest.test @@ -28,4 +28,4 @@ Updating a dev package should update to the latest available reference --RUN-- update --EXPECT-- -Updating a/a (dev-master def000 => dev-master abc123) +Upgrading a/a (dev-master def000 => dev-master abc123) diff --git a/tests/Composer/Test/Fixtures/installer/update-reference.test b/tests/Composer/Test/Fixtures/installer/update-reference.test index 01ba528fe..7836cbf5a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-reference.test +++ b/tests/Composer/Test/Fixtures/installer/update-reference.test @@ -27,4 +27,4 @@ Updates a dev package forcing it's reference --RUN-- install --EXPECT-- -Updating a/a (dev-master abc123 => dev-master def000) +Upgrading a/a (dev-master abc123 => dev-master def000) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test index aaa29cd6e..3fb16d52f 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test @@ -49,5 +49,5 @@ Update with a package whitelist only updates those packages if they are not pres --RUN-- update whitelisted dependency --EXPECT-- -Updating dependency (1.0.0 => 1.1.0) -Updating whitelisted (1.0.0 => 1.1.0) +Upgrading dependency (1.0.0 => 1.1.0) +Upgrading whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test index a76937e43..cbf350f0b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test @@ -60,6 +60,6 @@ Update with a package whitelist pattern and all-dependencies flag updates packag --RUN-- update whitelisted-* --with-all-dependencies --EXPECT-- -Updating whitelisted-component1 (1.0.0 => 1.1.0) -Updating dependency (1.0.0 => 1.1.0) -Updating whitelisted-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted-component1 (1.0.0 => 1.1.0) +Upgrading dependency (1.0.0 => 1.1.0) +Upgrading whitelisted-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test index 3cd619d22..68c1218c3 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test @@ -62,6 +62,6 @@ Update with a package whitelist only updates those packages and their dependenci --RUN-- update whitelisted-* --with-dependencies --EXPECT-- -Updating whitelisted-component1 (1.0.0 => 1.1.0) -Updating dependency (1.0.0 => 1.1.0) -Updating whitelisted-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted-component1 (1.0.0 => 1.1.0) +Upgrading dependency (1.0.0 => 1.1.0) +Upgrading whitelisted-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test index c4e879df8..dab3d87e3 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test @@ -72,6 +72,6 @@ Update with a package whitelist only updates those packages and their dependenci --RUN-- update whitelisted-* --with-dependencies --EXPECT-- -Updating dependency (1.0.0 => 1.1.0) -Updating whitelisted-component2 (1.0.0 => 1.1.0) -Updating whitelisted-component1 (1.0.0 => 1.1.0) +Upgrading dependency (1.0.0 => 1.1.0) +Upgrading whitelisted-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted-component1 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test index eafec1403..337fe3a9b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test @@ -57,5 +57,5 @@ Update with a package whitelist only updates those packages matching the pattern --RUN-- update whitelisted-* --EXPECT-- -Updating whitelisted-component1 (1.0.0 => 1.1.0) -Updating whitelisted-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted-component1 (1.0.0 => 1.1.0) +Upgrading whitelisted-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test index 54da86c8e..8e4fd2034 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test @@ -63,7 +63,7 @@ Update with a package whitelist only updates those corresponding to the pattern --RUN-- update vendor/Test* exact/Test-Package notexact/Test all/* no/reg?xp --EXPECT-- -Updating all/Package1 (1.0 => 2.0) -Updating all/Package2 (1.0 => 2.0) -Updating exact/Test-Package (1.0 => 2.0) -Updating vendor/Test-Package (1.0 => 2.0) +Upgrading all/Package1 (1.0 => 2.0) +Upgrading all/Package2 (1.0 => 2.0) +Upgrading exact/Test-Package (1.0 => 2.0) +Upgrading vendor/Test-Package (1.0 => 2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test index 1c2a68a10..ef7ca56c0 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test @@ -43,6 +43,6 @@ Limited update takes rules from lock if available, and not from the installed re --RUN-- update toupdate/installed --EXPECT-- -Updating old/installed (0.9.0 => 1.0.0) -Updating toupdate/installed (1.0.0 => 1.1.0) +Upgrading old/installed (0.9.0 => 1.0.0) +Upgrading toupdate/installed (1.0.0 => 1.1.0) Installing toupdate/notinstalled (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test index 93829c808..7b720e3ca 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -45,4 +45,4 @@ Update with a package whitelist removes unused packages update --with-dependencies whitelisted --EXPECT-- Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0 => 1.1.0) +Upgrading whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test index 2db3b6ead..02e772337 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test @@ -52,5 +52,5 @@ Update with a package whitelist only updates those packages and their dependenci --RUN-- update whitelisted --with-dependencies --EXPECT-- -Updating dependency (1.0.0 => 1.1.0) -Updating whitelisted (1.0.0 => 1.1.0) +Upgrading dependency (1.0.0 => 1.1.0) +Upgrading whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index 4dfb95a93..826f9fce1 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -54,4 +54,4 @@ Update with a package whitelist only updates those packages listed as command ar --RUN-- update whitelisted --EXPECT-- -Updating whitelisted (1.0.0 => 1.1.0) +Upgrading whitelisted (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index f761abe38..a950b247a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -50,13 +50,13 @@ update b/b --with-all-dependencies Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 2 updates, 0 removals - - Updating a/a (1.0.0 => 1.1.0) - - Updating b/b (1.0.0 => 1.1.0) + - Upgrading a/a (1.0.0 => 1.1.0) + - Upgrading b/b (1.0.0 => 1.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- -Updating a/a (1.0.0 => 1.1.0) -Updating b/b (1.0.0 => 1.1.0) +Upgrading a/a (1.0.0 => 1.1.0) +Upgrading b/b (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index e9f6d825a..5d2fc64c8 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -42,4 +42,4 @@ Installing locked dev packages should remove old dependencies install --EXPECT-- Uninstalling a/dependency (dev-master ref) -Updating a/devpackage (dev-master oldref => dev-master newref) +Upgrading a/devpackage (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test index f3aa1816e..c84ab3525 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -65,4 +65,4 @@ update "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master oldref => dev-master newref) +Upgrading a/a (dev-master oldref => dev-master newref) From 7e142b5d6bd6baea44edad2f48e4c4bbbdc6407f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 11 Feb 2020 17:51:02 +0100 Subject: [PATCH 241/321] Use Upgrading for package installs too --- src/Composer/Downloader/FileDownloader.php | 2 +- src/Composer/Downloader/VcsDownloader.php | 2 +- src/Composer/Installer/MetapackageInstaller.php | 2 +- tests/Composer/Test/Downloader/GitDownloaderTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 9775c27d9..357c5b59d 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -271,7 +271,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $from = $initial->getFullPrettyVersion(); $to = $target->getFullPrettyVersion(); - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading'; + $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); $this->remove($initial, $path, false); diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index 4992bbe6a..2236c4d88 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -165,7 +165,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa $to = $target->getFullPrettyVersion(); } - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading'; + $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); $urls = $this->prepareUrls($target->getSourceUrls()); diff --git a/src/Composer/Installer/MetapackageInstaller.php b/src/Composer/Installer/MetapackageInstaller.php index 1ed6beb71..fbd983fa3 100644 --- a/src/Composer/Installer/MetapackageInstaller.php +++ b/src/Composer/Installer/MetapackageInstaller.php @@ -93,7 +93,7 @@ class MetapackageInstaller implements InstallerInterface $name = $target->getName(); $from = $initial->getFullPrettyVersion(); $to = $target->getFullPrettyVersion(); - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading'; + $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . ")"); $repo->removePackage($initial); diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index ea8defea4..8863a6473 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -691,7 +691,7 @@ composer https://github.com/old/url (push) $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $ioMock->expects($this->at(0)) ->method('writeError') - ->with($this->stringContains('Updating')); + ->with($this->stringContains('Upgrading')); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock($ioMock, null, $processExecutor); From 71855e6430952f2c2f5883215da8d135d4ec6ca7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 22:10:39 +0100 Subject: [PATCH 242/321] Restore PRE/POST_DEPENDENCY_SOLVING events --- .../EventDispatcher/EventDispatcher.php | 6 ++- src/Composer/Installer.php | 15 +++--- src/Composer/Installer/InstallerEvent.php | 48 +++++++++---------- .../EventDispatcher/EventDispatcherTest.php | 7 +-- .../Test/Installer/InstallerEventTest.php | 10 ++-- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index b2ecdf023..34a1981a9 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -14,6 +14,8 @@ namespace Composer\EventDispatcher; use Composer\DependencyResolver\PolicyInterface; use Composer\DependencyResolver\Request; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\Transaction; use Composer\Installer\InstallerEvent; use Composer\IO\IOInterface; use Composer\Composer; @@ -128,9 +130,9 @@ class EventDispatcher * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $localRepo, Request $request, array $operations = array()) + public function dispatchInstallerEvent($eventName, $devMode, RepositorySet $repositorySet, Pool $pool, Request $request, PolicyInterface $policy, Transaction $transaction = null) { - return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $localRepo, $request, $operations)); + return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $repositorySet, $pool, $request, $policy, $transaction)); } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9d67e7d83..dbd3de015 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -381,10 +381,8 @@ class Installer } } - // TODO reenable events - //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); - $pool = $repositorySet->createPool($request); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -403,7 +401,7 @@ class Installer } // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? - //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $lockedRepository, $request, $lockTransaction); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy, $lockTransaction); $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); $this->io->writeError("Analyzed ".$ruleSetSize." rules to resolve dependencies", true, IOInterface::VERBOSE); @@ -526,11 +524,11 @@ class Installer $pool = $repositorySet->createPool($request); - //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $repositorySet, $pool, $request, $policy); $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); - //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $repositorySet, $pool, $request, $policy, $nonDevLockTransaction); $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); @@ -580,9 +578,8 @@ class Installer $request->requireName($link->getTarget(), $link->getConstraint()); } - //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); - $pool = $repositorySet->createPool($request); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -604,7 +601,7 @@ class Installer } // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? - //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy, $lockTransaction); } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? diff --git a/src/Composer/Installer/InstallerEvent.php b/src/Composer/Installer/InstallerEvent.php index 7752f6920..7fc5fe746 100644 --- a/src/Composer/Installer/InstallerEvent.php +++ b/src/Composer/Installer/InstallerEvent.php @@ -14,11 +14,11 @@ namespace Composer\Installer; use Composer\Composer; use Composer\DependencyResolver\PolicyInterface; -use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Request; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\Transaction; use Composer\EventDispatcher\Event; use Composer\IO\IOInterface; -use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositorySet; /** @@ -43,20 +43,15 @@ class InstallerEvent extends Event */ private $devMode; - /** - * @var PolicyInterface - */ - private $policy; - /** * @var RepositorySet */ private $repositorySet; /** - * @var RepositoryInterface + * @var Pool */ - private $localRepo; + private $pool; /** * @var Request @@ -64,9 +59,14 @@ class InstallerEvent extends Event private $request; /** - * @var OperationInterface[] + * @var PolicyInterface */ - private $operations; + private $policy; + + /** + * @var Transaction|null + */ + private $transaction; /** * Constructor. @@ -75,24 +75,24 @@ class InstallerEvent extends Event * @param Composer $composer * @param IOInterface $io * @param bool $devMode - * @param PolicyInterface $policy * @param RepositorySet $repositorySet - * @param RepositoryInterface $localRepo + * @param Pool $pool * @param Request $request - * @param OperationInterface[] $operations + * @param PolicyInterface $policy + * @param Transaction $transaction */ - public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $localRepo, Request $request, array $operations = array()) + public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, RepositorySet $repositorySet, Pool $pool, Request $request, PolicyInterface $policy, Transaction $transaction = null) { parent::__construct($eventName); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; - $this->policy = $policy; $this->repositorySet = $repositorySet; - $this->localRepo = $localRepo; + $this->pool = $pool; $this->request = $request; - $this->operations = $operations; + $this->policy = $policy; + $this->transaction = $transaction; } /** @@ -136,11 +136,11 @@ class InstallerEvent extends Event } /** - * @return RepositoryInterface + * @return Pool */ - public function getLocalRepo() + public function getPool() { - return $this->localRepo; + return $this->pool; } /** @@ -152,10 +152,10 @@ class InstallerEvent extends Event } /** - * @return OperationInterface[] + * @return Transaction|null */ - public function getOperations() + public function getTransaction() { - return $this->operations; + return $this->transaction; } } diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 1d6dcafe8..65a5ae62f 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -567,11 +567,12 @@ class EventDispatcherTest extends TestCase $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); $repositorySet = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); - $installedRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); + $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); + $transaction = $this->getMockBuilder('Composer\DependencyResolver\LockTransaction')->disableOriginalConstructor()->getMock(); $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); - $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, true, $policy, $repositorySet, $installedRepo, $request); - $dispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, true, $policy, $repositorySet, $installedRepo, $request, array()); + $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, true, $repositorySet, $pool, $request, $policy); + $dispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, true, $repositorySet, $pool, $request, $policy, $transaction); } public static function call() diff --git a/tests/Composer/Test/Installer/InstallerEventTest.php b/tests/Composer/Test/Installer/InstallerEventTest.php index 9be6c8121..52c7baa96 100644 --- a/tests/Composer/Test/Installer/InstallerEventTest.php +++ b/tests/Composer/Test/Installer/InstallerEventTest.php @@ -23,10 +23,10 @@ class InstallerEventTest extends TestCase $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); $repositorySet = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); - $localRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); + $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); + $transaction = $this->getMockBuilder('Composer\DependencyResolver\LockTransaction')->disableOriginalConstructor()->getMock(); $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); - $operations = array($this->getMockBuilder('Composer\DependencyResolver\Operation\OperationInterface')->getMock()); - $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, $policy, $repositorySet, $localRepo, $request, $operations); + $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, $repositorySet, $pool, $request, $policy, $transaction); $this->assertSame('EVENT_NAME', $event->getName()); $this->assertInstanceOf('Composer\Composer', $event->getComposer()); @@ -34,8 +34,8 @@ class InstallerEventTest extends TestCase $this->assertTrue($event->isDevMode()); $this->assertInstanceOf('Composer\DependencyResolver\PolicyInterface', $event->getPolicy()); $this->assertInstanceOf('Composer\Repository\RepositorySet', $event->getRepositorySet()); - $this->assertInstanceOf('Composer\Repository\RepositoryInterface', $event->getLocalRepo()); + $this->assertInstanceOf('Composer\DependencyResolver\Pool', $event->getPool()); $this->assertInstanceOf('Composer\DependencyResolver\Request', $event->getRequest()); - $this->assertCount(1, $event->getOperations()); + $this->assertInstanceOf('Composer\DependencyResolver\Transaction', $event->getTransaction()); } } From 835a91532d82c880ce111077e5953a0aa82f933e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 30 Jan 2020 22:14:19 +0100 Subject: [PATCH 243/321] Add PRE_POOL_CREATE event, fixes #8348 --- doc/articles/scripts.md | 2 + .../DependencyResolver/PoolBuilder.php | 24 ++- src/Composer/Installer.php | 6 +- src/Composer/Plugin/PluginEvents.php | 11 ++ src/Composer/Plugin/PrePoolCreateEvent.php | 158 ++++++++++++++++++ src/Composer/Repository/RepositorySet.php | 5 +- 6 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 src/Composer/Plugin/PrePoolCreateEvent.php diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index 751d3c492..c16977003 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -66,6 +66,8 @@ Composer fires the following named events during its execution process: - **pre-command-run**: occurs before a command is executed and allows you to manipulate the `InputInterface` object's options and arguments to tweak a command's behavior. +- **pre-pool-create**: occurs before the Pool of packages is created, and lets + you filter the list of packages which is going to enter the Solver. > **Note:** Composer makes no assumptions about the state of your dependencies > prior to `install` or `update`. Therefore, you should not specify scripts diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 360924143..d3d14e629 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -21,6 +21,9 @@ use Composer\Repository\PlatformRepository; use Composer\Repository\RootPackageRepository; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MultiConstraint; +use Composer\EventDispatcher\EventDispatcher; +use Composer\Plugin\PrePoolCreateEvent; +use Composer\Plugin\PluginEvents; /** * @author Nils Adermann @@ -31,6 +34,7 @@ class PoolBuilder private $stabilityFlags; private $rootAliases; private $rootReferences; + private $eventDispatcher; private $aliasMap = array(); private $nameConstraints = array(); @@ -38,12 +42,13 @@ class PoolBuilder private $packages = array(); private $unacceptableFixedPackages = array(); - public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences) + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, EventDispatcher $eventDispatcher = null) { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; + $this->eventDispatcher = $eventDispatcher; } public function buildPool(array $repositories, Request $request) @@ -132,6 +137,23 @@ class PoolBuilder } } + if ($this->eventDispatcher) { + $prePoolCreateEvent = new PrePoolCreateEvent( + PluginEvents::PRE_POOL_CREATE, + $repositories, + $request, + $this->acceptableStabilities, + $this->stabilityFlags, + $this->rootAliases, + $this->rootReferences, + $this->packages, + $this->unacceptableFixedPackages + ); + $this->eventDispatcher->dispatch($prePoolCreateEvent->getName(), $prePoolCreateEvent); + $this->packages = $prePoolCreateEvent->getPackages(); + $this->unacceptableFixedPackages = $prePoolCreateEvent->getUnacceptableFixedPackages(); + } + $pool = new Pool($this->packages, $this->unacceptableFixedPackages); $this->aliasMap = array(); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index dbd3de015..b7ee247db 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -381,7 +381,7 @@ class Installer } } - $pool = $repositorySet->createPool($request); + $pool = $repositorySet->createPool($request, $this->eventDispatcher); $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy); // solve dependencies @@ -522,7 +522,7 @@ class Installer $request->requireName($link->getTarget(), $link->getConstraint()); } - $pool = $repositorySet->createPool($request); + $pool = $repositorySet->createPool($request, $this->eventDispatcher); $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $repositorySet, $pool, $request, $policy); $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -578,7 +578,7 @@ class Installer $request->requireName($link->getTarget(), $link->getConstraint()); } - $pool = $repositorySet->createPool($request); + $pool = $repositorySet->createPool($request, $this->eventDispatcher); $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy); // solve dependencies diff --git a/src/Composer/Plugin/PluginEvents.php b/src/Composer/Plugin/PluginEvents.php index 1fb368baf..39cac6798 100644 --- a/src/Composer/Plugin/PluginEvents.php +++ b/src/Composer/Plugin/PluginEvents.php @@ -58,4 +58,15 @@ class PluginEvents * @var string */ const PRE_COMMAND_RUN = 'pre-command-run'; + + /** + * The PRE_POOL_CREATE event occurs before the Pool of packages is created, and lets + * you filter the list of packages which is going to enter the Solver + * + * The event listener method receives a + * Composer\Plugin\PrePoolCreateEvent instance. + * + * @var string + */ + const PRE_POOL_CREATE = 'pre-pool-create'; } diff --git a/src/Composer/Plugin/PrePoolCreateEvent.php b/src/Composer/Plugin/PrePoolCreateEvent.php new file mode 100644 index 000000000..0e6617739 --- /dev/null +++ b/src/Composer/Plugin/PrePoolCreateEvent.php @@ -0,0 +1,158 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +use Composer\EventDispatcher\Event; +use Symfony\Component\Console\Input\InputInterface; +use Composer\Repository\RepositoryInterface; +use Composer\DependencyResolver\Request; +use Composer\Package\PackageInterface; + +/** + * The pre command run event. + * + * @author Jordi Boggiano + */ +class PrePoolCreateEvent extends Event +{ + /** + * @var RepositoryInterface[] + */ + private $repositories; + /** + * @var Request + */ + private $request; + /** + * @var array + */ + private $acceptableStabilities; + /** + * @var array + */ + private $stabilityFlags; + /** + * @var array + */ + private $rootAliases; + /** + * @var array + */ + private $rootReferences; + /** + * @var PackageInterface[] + */ + private $packages; + /** + * @var PackageInterface[] + */ + private $unacceptableFixedPackages; + + /** + * @param string $name The event name + * @param RepositoryInterface[] $repositories + */ + public function __construct($name, array $repositories, Request $request, array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, array $packages, array $unacceptableFixedPackages) + { + parent::__construct($name); + + $this->repositories = $repositories; + $this->request = $request; + $this->acceptableStabilities = $acceptableStabilities; + $this->stabilityFlags = $stabilityFlags; + $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; + $this->packages = $packages; + $this->unacceptableFixedPackages = $unacceptableFixedPackages; + } + + /** + * @return RepositoryInterface[] + */ + public function getRepositories() + { + return $this->repositories; + } + + /** + * @return Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * @return array + */ + public function getAcceptableStabilities() + { + return $this->acceptableStabilities; + } + + /** + * @return array + */ + public function getStabilityFlags() + { + return $this->stabilityFlags; + } + + /** + * @return array + */ + public function getRootAliases() + { + return $this->rootAliases; + } + + /** + * @return array + */ + public function getRootReferences() + { + return $this->rootReferences; + } + + /** + * @return PackageInterface[] + */ + public function getPackages() + { + return $this->packages; + } + + /** + * @return PackageInterface[] + */ + public function getUnacceptableFixedPackages() + { + return $this->unacceptableFixedPackages; + } + + /** + * @param PackageInterface[] $packages + */ + public function setPackages(array $packages) + { + $this->packages = $packages; + } + + /** + * @param PackageInterface[] $packages + */ + public function setUnacceptableFixedPackages(array $packages) + { + $this->unacceptableFixedPackages = $packages; + } +} diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 80df9bb1d..6cafaa6c2 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -15,6 +15,7 @@ namespace Composer\Repository; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\PoolBuilder; use Composer\DependencyResolver\Request; +use Composer\EventDispatcher\EventDispatcher; use Composer\Package\BasePackage; use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; @@ -188,9 +189,9 @@ class RepositorySet * * @return Pool */ - public function createPool(Request $request) + public function createPool(Request $request, EventDispatcher $eventDispatcher = null) { - $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences); + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher); foreach ($this->repositories as $repo) { if ($repo instanceof InstalledRepositoryInterface && !$this->allowInstalledRepositories) { From d52ce3c37fdfd78baa86fc20019f0b47f6d5eb92 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 12 Feb 2020 14:35:31 +0100 Subject: [PATCH 244/321] Replace pre/post-dependencies-solving by a pre-operations-exec event happening only on install from lock --- doc/articles/scripts.md | 4 +- .../EventDispatcher/EventDispatcher.php | 15 ++-- src/Composer/Installer.php | 29 +++----- src/Composer/Installer/InstallerEvent.php | 68 +++---------------- src/Composer/Installer/InstallerEvents.php | 25 ++----- .../EventDispatcher/EventDispatcherTest.php | 7 +- .../Test/Installer/InstallerEventTest.php | 11 +-- 7 files changed, 34 insertions(+), 125 deletions(-) diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index c16977003..8977aa32d 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -43,8 +43,8 @@ Composer fires the following named events during its execution process: ### Installer Events -- **pre-dependencies-solving**: occurs before the dependencies are resolved. -- **post-dependencies-solving**: occurs after the dependencies have been resolved. +- **pre-operations-exec**: occurs before the install/upgrade/.. operations + are executed when installing a lock file. ### Package Events diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 34a1981a9..fda5c29fd 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -119,20 +119,17 @@ class EventDispatcher /** * Dispatch a installer event. * - * @param string $eventName The constant in InstallerEvents - * @param bool $devMode Whether or not we are in dev mode - * @param PolicyInterface $policy The policy - * @param RepositorySet $repositorySet The repository set - * @param RepositoryInterface $localRepo The installed repository - * @param Request $request The request - * @param array $operations The list of operations + * @param string $eventName The constant in InstallerEvents + * @param bool $devMode Whether or not we are in dev mode + * @param bool $executeOperations True if operations will be executed, false in --dry-run + * @param Transaction $transaction The transaction contains the list of operations * * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchInstallerEvent($eventName, $devMode, RepositorySet $repositorySet, Pool $pool, Request $request, PolicyInterface $policy, Transaction $transaction = null) + public function dispatchInstallerEvent($eventName, $devMode, $executeOperations, Transaction $transaction) { - return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $repositorySet, $pool, $request, $policy, $transaction)); + return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $executeOperations, $transaction)); } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index b7ee247db..a1f93695d 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -382,7 +382,6 @@ class Installer } $pool = $repositorySet->createPool($request, $this->eventDispatcher); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -400,9 +399,6 @@ class Installer return max(1, $e->getCode()); } - // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy, $lockTransaction); - $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); $this->io->writeError("Analyzed ".$ruleSetSize." rules to resolve dependencies", true, IOInterface::VERBOSE); @@ -524,11 +520,9 @@ class Installer $pool = $repositorySet->createPool($request, $this->eventDispatcher); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $repositorySet, $pool, $request, $policy); $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $repositorySet, $pool, $request, $policy, $nonDevLockTransaction); $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); @@ -547,22 +541,22 @@ class Installer */ protected function doInstall(RepositoryInterface $localRepo, $alreadySolved = false) { - $platformRepo = $this->createPlatformRepo(false); - $lockedRepository = $this->locker->getLockedRepository($this->devMode); - - // creating repository set - $policy = $this->createPolicy(false); - // use aliases from lock file only, so empty root aliases here - $repositorySet = $this->createRepositorySet(false, $platformRepo, array(), $lockedRepository); - $repositorySet->addRepository($lockedRepository); - $this->io->writeError('Installing dependencies from lock file'.($this->devMode ? ' (including require-dev)' : '').''); + $lockedRepository = $this->locker->getLockedRepository($this->devMode); + // verify that the lock file works with the current platform repository // we can skip this part if we're doing this as the second step after an update if (!$alreadySolved) { $this->io->writeError('Verifying lock file contents can be installed on current platform.'); + $platformRepo = $this->createPlatformRepo(false); + // creating repository set + $policy = $this->createPolicy(false); + // use aliases from lock file only, so empty root aliases here + $repositorySet = $this->createRepositorySet(false, $platformRepo, array(), $lockedRepository); + $repositorySet->addRepository($lockedRepository); + // creating requirements request $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); @@ -579,7 +573,6 @@ class Installer } $pool = $repositorySet->createPool($request, $this->eventDispatcher); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -599,13 +592,11 @@ class Installer return max(1, $e->getCode()); } - - // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $repositorySet, $pool, $request, $policy, $lockTransaction); } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? $localRepoTransaction = new LocalRepoTransaction($lockedRepository, $localRepo); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_OPERATIONS_EXEC, $this->devMode, $this->executeOperations, $localRepoTransaction); if (!$localRepoTransaction->getOperations()) { $this->io->writeError('Nothing to install, update or remove'); diff --git a/src/Composer/Installer/InstallerEvent.php b/src/Composer/Installer/InstallerEvent.php index 7fc5fe746..1b8f03de4 100644 --- a/src/Composer/Installer/InstallerEvent.php +++ b/src/Composer/Installer/InstallerEvent.php @@ -21,11 +21,6 @@ use Composer\EventDispatcher\Event; use Composer\IO\IOInterface; use Composer\Repository\RepositorySet; -/** - * An event for all installer. - * - * @author François Pluchino - */ class InstallerEvent extends Event { /** @@ -44,27 +39,12 @@ class InstallerEvent extends Event private $devMode; /** - * @var RepositorySet + * @var bool */ - private $repositorySet; + private $executeOperations; /** - * @var Pool - */ - private $pool; - - /** - * @var Request - */ - private $request; - - /** - * @var PolicyInterface - */ - private $policy; - - /** - * @var Transaction|null + * @var Transaction */ private $transaction; @@ -75,23 +55,17 @@ class InstallerEvent extends Event * @param Composer $composer * @param IOInterface $io * @param bool $devMode - * @param RepositorySet $repositorySet - * @param Pool $pool - * @param Request $request - * @param PolicyInterface $policy + * @param bool $executeOperations * @param Transaction $transaction */ - public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, RepositorySet $repositorySet, Pool $pool, Request $request, PolicyInterface $policy, Transaction $transaction = null) + public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, $executeOperations, Transaction $transaction) { parent::__construct($eventName); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; - $this->repositorySet = $repositorySet; - $this->pool = $pool; - $this->request = $request; - $this->policy = $policy; + $this->executeOperations = $executeOperations; $this->transaction = $transaction; } @@ -120,35 +94,11 @@ class InstallerEvent extends Event } /** - * @return PolicyInterface + * @return bool */ - public function getPolicy() + public function isExecutingOperations() { - return $this->policy; - } - - /** - * @return RepositorySet - */ - public function getRepositorySet() - { - return $this->repositorySet; - } - - /** - * @return Pool - */ - public function getPool() - { - return $this->pool; - } - - /** - * @return Request - */ - public function getRequest() - { - return $this->request; + return $this->executeOperations; } /** diff --git a/src/Composer/Installer/InstallerEvents.php b/src/Composer/Installer/InstallerEvents.php index e05c92587..f75b8bdd4 100644 --- a/src/Composer/Installer/InstallerEvents.php +++ b/src/Composer/Installer/InstallerEvents.php @@ -12,32 +12,15 @@ namespace Composer\Installer; -/** - * The Installer Events. - * - * @author François Pluchino - */ class InstallerEvents { /** - * The PRE_DEPENDENCIES_SOLVING event occurs as a installer begins - * resolve operations. + * The PRE_OPERATIONS_EXEC event occurs before the lock file gets + * installed and operations are executed. * - * The event listener method receives a - * Composer\Installer\InstallerEvent instance. + * The event listener method receives an Composer\Installer\InstallerEvent instance. * * @var string */ - const PRE_DEPENDENCIES_SOLVING = 'pre-dependencies-solving'; - - /** - * The POST_DEPENDENCIES_SOLVING event occurs as a installer after - * resolve operations. - * - * The event listener method receives a - * Composer\Installer\InstallerEvent instance. - * - * @var string - */ - const POST_DEPENDENCIES_SOLVING = 'post-dependencies-solving'; + const PRE_OPERATIONS_EXEC = 'pre-operations-exec'; } diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 65a5ae62f..c038681da 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -565,14 +565,9 @@ class EventDispatcherTest extends TestCase ->method('getListeners') ->will($this->returnValue(array())); - $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); - $repositorySet = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); - $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); $transaction = $this->getMockBuilder('Composer\DependencyResolver\LockTransaction')->disableOriginalConstructor()->getMock(); - $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); - $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, true, $repositorySet, $pool, $request, $policy); - $dispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, true, $repositorySet, $pool, $request, $policy, $transaction); + $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_OPERATIONS_EXEC, true, true, $transaction); } public static function call() diff --git a/tests/Composer/Test/Installer/InstallerEventTest.php b/tests/Composer/Test/Installer/InstallerEventTest.php index 52c7baa96..6c0689844 100644 --- a/tests/Composer/Test/Installer/InstallerEventTest.php +++ b/tests/Composer/Test/Installer/InstallerEventTest.php @@ -21,21 +21,14 @@ class InstallerEventTest extends TestCase { $composer = $this->getMockBuilder('Composer\Composer')->getMock(); $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $policy = $this->getMockBuilder('Composer\DependencyResolver\PolicyInterface')->getMock(); - $repositorySet = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); - $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); $transaction = $this->getMockBuilder('Composer\DependencyResolver\LockTransaction')->disableOriginalConstructor()->getMock(); - $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); - $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, $repositorySet, $pool, $request, $policy, $transaction); + $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, true, $transaction); $this->assertSame('EVENT_NAME', $event->getName()); $this->assertInstanceOf('Composer\Composer', $event->getComposer()); $this->assertInstanceOf('Composer\IO\IOInterface', $event->getIO()); $this->assertTrue($event->isDevMode()); - $this->assertInstanceOf('Composer\DependencyResolver\PolicyInterface', $event->getPolicy()); - $this->assertInstanceOf('Composer\Repository\RepositorySet', $event->getRepositorySet()); - $this->assertInstanceOf('Composer\DependencyResolver\Pool', $event->getPool()); - $this->assertInstanceOf('Composer\DependencyResolver\Request', $event->getRequest()); + $this->assertTrue($event->isExecutingOperations()); $this->assertInstanceOf('Composer\DependencyResolver\Transaction', $event->getTransaction()); } } From 23efda91557a474e28b823a44a0fb02234dd712d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 12 Feb 2020 14:53:22 +0100 Subject: [PATCH 245/321] Try to fix windows build --- tests/Composer/Test/Downloader/GitDownloaderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 8863a6473..429ad45df 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -578,7 +578,7 @@ composer https://github.com/old/url (push) public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover() { - $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer '/'"); + $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."'"); $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); From 6681f44f564b2c83466b2c92d103790a49b856a3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 09:08:04 +0100 Subject: [PATCH 246/321] Tweak appveyor build --- appveyor.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 922a20e75..15950da41 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ clone_depth: 5 environment: # This sets the PHP version (from Chocolatey) - PHPCI_CHOCO_VERSION: 7.3.1 + PHPCI_CHOCO_VERSION: 7.3.14 PHPCI_CACHE: C:\tools\phpci PHPCI_PHP: C:\tools\phpci\php PHPCI_COMPOSER: C:\tools\phpci\composer @@ -25,6 +25,15 @@ install: - IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%" - php -v - IF %PHP%==0 (composer --version) ELSE (composer self-update) + - IF %PHP%==0 cd %PHPCI_PHP% + - IF %PHP%==0 copy php.ini-production php.ini /Y + - IF %PHP%==0 echo date.timezone="UTC" >> php.ini + - IF %PHP%==0 echo extension_dir=ext >> php.ini + - IF %PHP%==0 echo extension=php_openssl.dll >> php.ini + - IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini + - IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini + - IF %PHP%==0 echo extension=php_intl.dll >> php.ini + - IF %PHP%==0 echo extension=php_curl.dll >> php.ini - cd %APPVEYOR_BUILD_FOLDER% - composer install --prefer-dist --no-progress From 5d8dc48bd42fdc50bdbce8737c64eb3d0e8d6159 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 10:05:42 +0100 Subject: [PATCH 247/321] Fix test on windows --- tests/Composer/Test/Downloader/GitDownloaderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 429ad45df..4c1e37806 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -578,7 +578,7 @@ composer https://github.com/old/url (push) public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover() { - $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer '".(Platform::isWindows() ? 'C:\\' : '/')."'"); + $expectedFirstGitUpdateCommand = $this->winCompat("git remote set-url composer '".(Platform::isWindows() ? 'C:\\\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer '".(Platform::isWindows() ? 'C:\\\\' : '/')."'"); $expectedSecondGitUpdateCommand = $this->winCompat("git remote set-url composer 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer); git remote set-url composer 'https://github.com/composer/composer'"); $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); From 44d1e15294058129b4b27008d3d0ffb25a896c15 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 16:02:31 +0100 Subject: [PATCH 248/321] Simplify suggester output when updating, refactor suggest command to reuse SuggestedPackagesReporter and make smarter defaults, fixes #6267 --- doc/03-cli.md | 11 +- src/Composer/Command/InstallCommand.php | 2 - src/Composer/Command/RequireCommand.php | 2 - src/Composer/Command/SuggestsCommand.php | 130 +++++------------- src/Composer/Command/UpdateCommand.php | 2 - src/Composer/Installer.php | 24 +--- .../Installer/SuggestedPackagesReporter.php | 101 ++++++++++++-- .../installer/suggest-prod-nolock.test | 32 +++++ .../Test/Fixtures/installer/suggest-prod.test | 27 ++-- .../installer/suggest-uninstalled.test | 2 +- .../SuggestedPackagesReporterTest.php | 85 ++++++++---- 11 files changed, 241 insertions(+), 177 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test diff --git a/doc/03-cli.md b/doc/03-cli.md index 296cb101b..71154fa40 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -106,7 +106,6 @@ resolution. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--no-suggest:** Skips suggested packages in the output. * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. @@ -156,7 +155,6 @@ php composer.phar update "vendor/*" * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--no-suggest:** Skips suggested packages in the output. * **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements. * **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements. * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster @@ -203,7 +201,6 @@ If you do not specify a package, composer will prompt you to search for a packag * **--prefer-dist:** Install packages from `dist` when available. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--no-suggest:** Skips suggested packages in the output. * **--no-update:** Disables the automatic update of the dependencies. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--update-no-dev:** Run the dependency update with the `--no-dev` option. @@ -410,16 +407,16 @@ Lists all packages suggested by currently installed set of packages. You can optionally pass one or multiple package names in the format of `vendor/package` to limit output to suggestions made by those packages only. -Use the `--by-package` or `--by-suggestion` flags to group the output by +Use the `--by-package` (default) or `--by-suggestion` flags to group the output by the package offering the suggestions or the suggested packages respectively. -Use the `--verbose (-v)` flag to display the suggesting package and the suggestion reason. -This implies `--by-package --by-suggestion`, showing both lists. +If you only want a list of suggested package names, use `--list`. ### Options -* **--by-package:** Groups output by suggesting package. +* **--by-package:** Groups output by suggesting package (default). * **--by-suggestion:** Groups output by suggested package. +* **--list:** Show only list of suggested package names. * **--no-dev:** Excludes suggestions from `require-dev` packages. ## depends (why) diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 1a2c808db..d059928cc 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -44,7 +44,6 @@ class InstallCommand extends BaseCommand new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), - new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), @@ -107,7 +106,6 @@ EOT ->setDevMode(!$input->getOption('no-dev')) ->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setRunScripts(!$input->getOption('no-scripts')) - ->setSkipSuggest($input->getOption('no-suggest')) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index a881e7de1..0118aa959 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -55,7 +55,6 @@ class RequireCommand extends InitCommand new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'), new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), - new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'), new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), @@ -259,7 +258,6 @@ EOT ->setPreferDist($input->getOption('prefer-dist')) ->setDevMode($updateDevMode) ->setRunScripts(!$input->getOption('no-scripts')) - ->setSkipSuggest($input->getOption('no-suggest')) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 6c2619751..07ca1ce00 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -13,6 +13,9 @@ namespace Composer\Command; use Composer\Repository\PlatformRepository; +use Composer\Repository\RootPackageRepository; +use Composer\Repository\CompositeRepository; +use Composer\Installer\SuggestedPackagesReporter; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -26,8 +29,9 @@ class SuggestsCommand extends BaseCommand ->setName('suggests') ->setDescription('Shows package suggestions.') ->setDefinition(array( - new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package'), + new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package (default)'), new InputOption('by-suggestion', null, InputOption::VALUE_NONE, 'Groups output by suggested package'), + new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'), )) @@ -36,8 +40,6 @@ class SuggestsCommand extends BaseCommand The %command.name% command shows a sorted list of suggested packages. -Enabling -v implies --by-package --by-suggestion, showing both lists. - Read more at https://getcomposer.org/doc/03-cli.md#suggests EOT ) @@ -49,108 +51,50 @@ EOT */ protected function execute(InputInterface $input, OutputInterface $output) { - $lock = $this->getComposer()->getLocker()->getLockData(); + $composer = $this->getComposer(); - if (empty($lock)) { - throw new \RuntimeException('Lockfile seems to be empty?'); + $installedRepos = array( + new RootPackageRepository(array(clone $composer->getPackage())), + ); + + $locker = $composer->getLocker(); + if ($locker->isLocked()) { + $installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides()); + $installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev')); + } else { + $installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array()); + $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); } - $packages = $lock['packages']; - - if (!$input->getOption('no-dev')) { - $packages += $lock['packages-dev']; - } + $installedRepo = new CompositeRepository($installedRepos); + $reporter = new SuggestedPackagesReporter($this->getIO()); $filter = $input->getArgument('packages'); - - // First assemble lookup list of packages that are installed, replaced or provided - $installed = array(); - foreach ($packages as $package) { - $installed[] = $package['name']; - - if (!empty($package['provide'])) { - $installed = array_merge($installed, array_keys($package['provide'])); - } - - if (!empty($package['replace'])) { - $installed = array_merge($installed, array_keys($package['replace'])); - } - } - - // Undub and sort the install list into a sorted lookup array - $installed = array_flip($installed); - ksort($installed); - - // Init platform repo - $platform = new PlatformRepository(array(), $this->getComposer()->getConfig()->get('platform') ?: array()); - - // Next gather all suggestions that are not in that list - $suggesters = array(); - $suggested = array(); - foreach ($packages as $package) { - $packageName = $package['name']; - if ((!empty($filter) && !in_array($packageName, $filter)) || empty($package['suggest'])) { + foreach ($installedRepo->getPackages() as $package) { + if (!empty($filter) && !in_array($package->getName(), $filter)) { continue; } - foreach ($package['suggest'] as $suggestion => $reason) { - if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $suggestion) && null !== $platform->findPackage($suggestion, '*')) { - continue; - } - if (!isset($installed[$suggestion])) { - $suggesters[$packageName][$suggestion] = $reason; - $suggested[$suggestion][$packageName] = $reason; - } - } - } - ksort($suggesters); - ksort($suggested); - // Determine output mode - $mode = 0; + $reporter->addSuggestionsFromPackage($package); + } + + // Determine output mode, default is by-package + $mode = SuggestedPackagesReporter::MODE_BY_PACKAGE; $io = $this->getIO(); - if ($input->getOption('by-package') || $io->isVerbose()) { - $mode |= 1; - } + // if by-suggestion is given we override the default if ($input->getOption('by-suggestion')) { - $mode |= 2; + $mode = SuggestedPackagesReporter::MODE_BY_SUGGESTION; + } + // unless by-package is also present then we enable both + if ($input->getOption('by-package')) { + $mode |= SuggestedPackagesReporter::MODE_BY_PACKAGE; + } + // list is exclusive and overrides everything else + if ($input->getOption('list')) { + $mode = SuggestedPackagesReporter::MODE_LIST; } - // Simple mode - if ($mode === 0) { - foreach (array_keys($suggested) as $suggestion) { - $io->write(sprintf('%s', $suggestion)); - } - - return 0; - } - - // Grouped by package - if ($mode & 1) { - foreach ($suggesters as $suggester => $suggestions) { - $io->write(sprintf('%s suggests:', $suggester)); - - foreach ($suggestions as $suggestion => $reason) { - $io->write(sprintf(' - %s: %s', $suggestion, $reason ?: '*')); - } - $io->write(''); - } - } - - // Grouped by suggestion - if ($mode & 2) { - // Improve readability in full mode - if ($mode & 1) { - $io->write(str_repeat('-', 78)); - } - foreach ($suggested as $suggestion => $suggesters) { - $io->write(sprintf('%s is suggested by:', $suggestion)); - - foreach ($suggesters as $suggester => $reason) { - $io->write(sprintf(' - %s: %s', $suggester, $reason ?: '*')); - } - $io->write(''); - } - } + $reporter->output($mode, $installedRepo); return 0; } diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 0c3d3e6c7..a6a5dc4f7 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -48,7 +48,6 @@ class UpdateCommand extends BaseCommand new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), - new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'), new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of whitelisted packages to the whitelist, except those defined in root package.'), new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist, including those defined in root package.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), @@ -154,7 +153,6 @@ EOT ->setDevMode(!$input->getOption('no-dev')) ->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setRunScripts(!$input->getOption('no-scripts')) - ->setSkipSuggest($input->getOption('no-suggest')) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index a1f93695d..627adee76 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -131,7 +131,6 @@ class Installer protected $ignorePlatformReqs = false; protected $preferStable = false; protected $preferLowest = false; - protected $skipSuggest = false; protected $writeLock; protected $executeOperations = true; @@ -257,9 +256,13 @@ class Installer $this->installationManager->notifyInstalls($this->io); } - // output suggestions if we're in dev mode - if ($this->update && $this->devMode && !$this->skipSuggest) { - $this->suggestedPackagesReporter->output($this->locker->getLockedRepository($this->devMode)); + if ($this->update) { + $installedRepos = array( + $this->locker->getLockedRepository($this->devMode), + $this->createPlatformRepo(false), + new RootPackageRepository(array(clone $this->package)), + ); + $this->suggestedPackagesReporter->outputMinimalistic(new CompositeRepository($installedRepos)); } // Find abandoned packages and warn user @@ -1310,19 +1313,6 @@ class Installer return $this; } - /** - * Should suggestions be skipped? - * - * @param bool $skipSuggest - * @return Installer - */ - public function setSkipSuggest($skipSuggest = true) - { - $this->skipSuggest = (bool) $skipSuggest; - - return $this; - } - /** * Disables plugins. * diff --git a/src/Composer/Installer/SuggestedPackagesReporter.php b/src/Composer/Installer/SuggestedPackagesReporter.php index 023aeb91d..c40bbd2c0 100644 --- a/src/Composer/Installer/SuggestedPackagesReporter.php +++ b/src/Composer/Installer/SuggestedPackagesReporter.php @@ -24,6 +24,10 @@ use Symfony\Component\Console\Formatter\OutputFormatter; */ class SuggestedPackagesReporter { + const MODE_LIST = 1; + const MODE_BY_PACKAGE = 2; + const MODE_BY_SUGGESTION = 4; + /** * @var array */ @@ -91,38 +95,105 @@ class SuggestedPackagesReporter /** * Output suggested packages. + * * Do not list the ones already installed if installed repository provided. * - * @param RepositoryInterface $installedRepo Installed packages + * @param int $mode One of the MODE_* constants from this class * @return SuggestedPackagesReporter */ - public function output(RepositoryInterface $lockedRepo = null) + public function output($mode, RepositoryInterface $installedRepo = null) + { + $suggestedPackages = $this->getFilteredSuggestions($installedRepo); + + $suggesters = array(); + $suggested = array(); + foreach ($suggestedPackages as $suggestion) { + $suggesters[$suggestion['source']][$suggestion['target']] = $suggestion['reason']; + $suggested[$suggestion['target']][$suggestion['source']] = $suggestion['reason']; + } + ksort($suggesters); + ksort($suggested); + + // Simple mode + if ($mode & self::MODE_LIST) { + foreach (array_keys($suggested) as $name) { + $this->io->write(sprintf('%s', $name)); + } + + return 0; + } + + // Grouped by package + if ($mode & self::MODE_BY_PACKAGE) { + foreach ($suggesters as $suggester => $suggestions) { + $this->io->write(sprintf('%s suggests:', $suggester)); + + foreach ($suggestions as $suggestion => $reason) { + $this->io->write(sprintf(' - %s' . ($reason ? ': %s' : ''), $suggestion, $this->escapeOutput($reason))); + } + $this->io->write(''); + } + } + + // Grouped by suggestion + if ($mode & self::MODE_BY_SUGGESTION) { + // Improve readability in full mode + if ($mode & self::MODE_BY_PACKAGE) { + $this->io->write(str_repeat('-', 78)); + } + foreach ($suggested as $suggestion => $suggesters) { + $this->io->write(sprintf('%s is suggested by:', $suggestion)); + + foreach ($suggesters as $suggester => $reason) { + $this->io->write(sprintf(' - %s' . ($reason ? ': %s' : ''), $suggester, $this->escapeOutput($reason))); + } + $this->io->write(''); + } + } + + return $this; + } + + /** + * Output number of new suggested packages and a hint to use suggest command. + ** + * Do not list the ones already installed if installed repository provided. + * + * @return SuggestedPackagesReporter + */ + public function outputMinimalistic(RepositoryInterface $installedRepo = null) + { + $suggestedPackages = $this->getFilteredSuggestions($installedRepo); + if ($suggestedPackages) { + $this->io->writeError(count($suggestedPackages).' package suggestions were added by new dependencies, use composer suggest to see details.'); + } + + return $this; + } + + private function getFilteredSuggestions(RepositoryInterface $installedRepo = null) { $suggestedPackages = $this->getPackages(); - $lockedPackages = array(); - if (null !== $lockedRepo && ! empty($suggestedPackages)) { - foreach ($lockedRepo->getPackages() as $package) { - $lockedPackages = array_merge( - $lockedPackages, + $installedNames = array(); + if (null !== $installedRepo && !empty($suggestedPackages)) { + foreach ($installedRepo->getPackages() as $package) { + $installedNames = array_merge( + $installedNames, $package->getNames() ); } } + $suggestions = array(); foreach ($suggestedPackages as $suggestion) { - if (in_array($suggestion['target'], $lockedPackages)) { + if (in_array($suggestion['target'], $installedNames)) { continue; } - $this->io->writeError(sprintf( - '%s suggests installing %s%s', - $suggestion['source'], - $this->escapeOutput($suggestion['target']), - $this->escapeOutput('' !== $suggestion['reason'] ? ' ('.$suggestion['reason'].')' : '') - )); + $suggestions[] = $suggestion; } - return $this; + return $suggestions; } /** diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test b/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test new file mode 100644 index 000000000..82bf8558d --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test @@ -0,0 +1,32 @@ +--TEST-- +Suggestions are displayed even in non-dev mode for new suggesters installed when updating the lock file +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" } } + ] + } + ], + "require": { + "a/a": "1.0.0" + } +} +--RUN-- +install --no-dev +--EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. +Loading composer repositories with package information +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Locking a/a (1.0.0) +Writing lock file +Installing dependencies from lock file +Package operations: 1 install, 0 updates, 0 removals +1 package suggestions were added by new dependencies, use composer suggest to see details. +Generating autoload files + +--EXPECT-- +Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index 6a00b5607..ed2023504 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -1,5 +1,5 @@ --TEST-- -Suggestions are not displayed in non-dev mode +Suggestions are not displayed for when not updating the lock file --COMPOSER-- { "repositories": [ @@ -14,16 +14,25 @@ Suggestions are not displayed in non-dev mode "a/a": "1.0.0" } } +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- -install --no-dev +install --EXPECT-OUTPUT-- -No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. -Loading composer repositories with package information -Updating dependencies -Lock file operations: 1 install, 0 updates, 0 removals - - Locking a/a (1.0.0) -Writing lock file -Installing dependencies from lock file +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. Package operations: 1 install, 0 updates, 0 removals Generating autoload files diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index 1b88b2d8b..97e05b713 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -25,7 +25,7 @@ Lock file operations: 1 install, 0 updates, 0 removals Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals -a/a suggests installing b/b (an obscure reason) +1 package suggestions were added by new dependencies, use composer suggest to see details. Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php index 03bb0b5fb..286b386d9 100644 --- a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php +++ b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php @@ -33,14 +33,13 @@ class SuggestedPackagesReporterTest extends TestCase /** * @covers ::__construct */ - public function testContrsuctor() + public function testConstructor() { $this->io->expects($this->once()) - ->method('writeError'); + ->method('write'); - $suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); - $suggestedPackagesReporter->addPackage('a', 'b', 'c'); - $suggestedPackagesReporter->output(); + $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_LIST); } /** @@ -135,25 +134,33 @@ class SuggestedPackagesReporterTest extends TestCase { $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); - $this->io->expects($this->once()) - ->method('writeError') - ->with('a suggests installing b (c)'); + $this->io->expects($this->at(0)) + ->method('write') + ->with('a suggests:'); - $this->suggestedPackagesReporter->output(); + $this->io->expects($this->at(1)) + ->method('write') + ->with(' - b: c'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } /** * @covers ::output */ - public function testOutputWithNoSuggestedPackage() + public function testOutputWithNoSuggestionReason() { $this->suggestedPackagesReporter->addPackage('a', 'b', ''); - $this->io->expects($this->once()) - ->method('writeError') - ->with('a suggests installing b'); + $this->io->expects($this->at(0)) + ->method('write') + ->with('a suggests:'); - $this->suggestedPackagesReporter->output(); + $this->io->expects($this->at(1)) + ->method('write') + ->with(' - b'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } /** @@ -165,14 +172,18 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('source', 'target2', "Like us on Facebook"); $this->io->expects($this->at(0)) - ->method('writeError') - ->with("source suggests installing target1 ([1;37;42m Like us on Facebook [0m)"); + ->method('write') + ->with('source suggests:'); $this->io->expects($this->at(1)) - ->method('writeError') - ->with('source suggests installing target2 (\\Like us on Facebook\\)'); + ->method('write') + ->with(' - target1: [1;37;42m Like us on Facebook [0m'); - $this->suggestedPackagesReporter->output(); + $this->io->expects($this->at(2)) + ->method('write') + ->with(' - target2: \\Like us on Facebook\\'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } /** @@ -184,14 +195,26 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons'); $this->io->expects($this->at(0)) - ->method('writeError') - ->with('a suggests installing b (c)'); + ->method('write') + ->with('a suggests:'); $this->io->expects($this->at(1)) - ->method('writeError') - ->with('source package suggests installing target (because reasons)'); + ->method('write') + ->with(' - b: c'); - $this->suggestedPackagesReporter->output(); + $this->io->expects($this->at(2)) + ->method('write') + ->with(''); + + $this->io->expects($this->at(3)) + ->method('write') + ->with('source package suggests:'); + + $this->io->expects($this->at(4)) + ->method('write') + ->with(' - target: because reasons'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } /** @@ -221,11 +244,15 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons'); - $this->io->expects($this->once()) - ->method('writeError') - ->with('source package suggests installing target (because reasons)'); + $this->io->expects($this->at(0)) + ->method('write') + ->with('source package suggests:'); - $this->suggestedPackagesReporter->output($repository); + $this->io->expects($this->at(1)) + ->method('write') + ->with(' - target: because reasons'); + + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE, $repository); } /** @@ -237,7 +264,7 @@ class SuggestedPackagesReporterTest extends TestCase $repository->expects($this->exactly(0)) ->method('getPackages'); - $this->suggestedPackagesReporter->output($repository); + $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE, $repository); } private function getSuggestedPackageArray() From 71a98d8a7ef2353a034d270bfc6514de0929fc98 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 16:22:53 +0100 Subject: [PATCH 249/321] Remove --no-suggest flag as it is gone --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 00e227954..8c0ee0bc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,7 +57,7 @@ before_install: install: # flags to pass to install - - flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-suggest --no-progress" + - flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-progress" # update deps to latest in case of high deps build - if [ "$deps" == "high" ]; then composer config platform.php 7.4.0; composer update $flags; fi # install dependencies using system provided composer binary From f35cd8948aa69438ceb597553225d48de12612a7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 16:29:57 +0100 Subject: [PATCH 250/321] Minor refactoring of RootPackageRepo --- src/Composer/Command/SuggestsCommand.php | 2 +- src/Composer/Installer.php | 4 ++-- src/Composer/Repository/RootPackageRepository.php | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 07ca1ce00..93c0e1e30 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -54,7 +54,7 @@ EOT $composer = $this->getComposer(); $installedRepos = array( - new RootPackageRepository(array(clone $composer->getPackage())), + new RootPackageRepository(clone $composer->getPackage()), ); $locker = $composer->getLocker(); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 627adee76..46b7ea1ea 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -260,7 +260,7 @@ class Installer $installedRepos = array( $this->locker->getLockedRepository($this->devMode), $this->createPlatformRepo(false), - new RootPackageRepository(array(clone $this->package)), + new RootPackageRepository(clone $this->package), ); $this->suggestedPackagesReporter->outputMinimalistic(new CompositeRepository($installedRepos)); } @@ -706,7 +706,7 @@ class Installer $this->fixedRootPackage->setDevRequires(array()); $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $rootAliases, $this->package->getReferences(), $rootRequires); - $repositorySet->addRepository(new RootPackageRepository(array($this->fixedRootPackage))); + $repositorySet->addRepository(new RootPackageRepository($this->fixedRootPackage)); $repositorySet->addRepository($platformRepo); if ($this->additionalFixedRepository) { $repositorySet->addRepository($this->additionalFixedRepository); diff --git a/src/Composer/Repository/RootPackageRepository.php b/src/Composer/Repository/RootPackageRepository.php index 721737fdc..9652282f7 100644 --- a/src/Composer/Repository/RootPackageRepository.php +++ b/src/Composer/Repository/RootPackageRepository.php @@ -12,6 +12,8 @@ namespace Composer\Repository; +use Composer\Package\RootPackageInterface; + /** * Root package repository. * @@ -21,6 +23,11 @@ namespace Composer\Repository; */ class RootPackageRepository extends ArrayRepository { + public function __construct(RootPackageInterface $package) + { + parent::__construct(array($package)); + } + public function getRepoName() { return 'root package repo'; From c5c6d44a0b094e0fefa81f10fed4c72a1ecaf7c9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 17:51:22 +0100 Subject: [PATCH 251/321] Refactor away some unnecessary RepositorySet usages --- .../Command/BaseDependencyCommand.php | 11 +++-- src/Composer/Plugin/PluginManager.php | 42 +++++++++++-------- .../Test/Plugin/PluginInstallerTest.php | 14 +++---- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index be7703f02..dfacb0639 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -20,7 +20,6 @@ use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; -use Composer\Repository\RepositorySet; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Helper\Table; @@ -78,8 +77,6 @@ class BaseDependencyCommand extends BaseCommand $composer->getRepositoryManager()->getLocalRepository(), new PlatformRepository(array(), $platformOverrides), )); - $repositorySet = new RepositorySet(); - $repositorySet->addRepository($repository); // Parse package name and constraint list($needle, $textConstraint) = array_pad( @@ -87,9 +84,15 @@ class BaseDependencyCommand extends BaseCommand 2, $input->getArgument(self::ARGUMENT_CONSTRAINT) ); + $needle = strtolower($needle); // Find packages that are or provide the requested package first - $packages = $repositorySet->findPackages(strtolower($needle), null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); + $packages = array(); + foreach ($repository->getPackages() as $package) { + if (in_array($needle, $package->getNames(), true)) { + $packages[] = $package; + } + } if (empty($packages)) { throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index a706bf8ad..cc0b39da9 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -21,7 +21,6 @@ use Composer\Package\Version\VersionParser; use Composer\Repository\RepositoryInterface; use Composer\Package\PackageInterface; use Composer\Package\Link; -use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\Plugin\Capability\Capability; use Composer\Util\PackageSorter; @@ -158,14 +157,13 @@ class PluginManager $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $repositorySet = new RepositorySet('dev'); - $repositorySet->addRepository($localRepo); + $localRepos = $localRepo; if ($globalRepo) { - $repositorySet->addRepository($globalRepo); + $localRepos = new CompositeRepository(array($localRepos, $globalRepo)); } $autoloadPackages = array($package->getName() => $package); - $autoloadPackages = $this->collectDependencies($repositorySet, $autoloadPackages, $package); + $autoloadPackages = $this->collectDependencies($localRepos, $autoloadPackages, $package); $generator = $this->composer->getAutoloadGenerator(); $autoloads = array(); @@ -376,13 +374,13 @@ class PluginManager /** * Recursively generates a map of package names to packages for all deps * - * @param RepositorySet $repositorySet Repository set of installed packages - * @param array $collected Current state of the map for recursion - * @param PackageInterface $package The package to analyze + * @param RepositoryInterface $localRepos Set of local repos + * @param array $collected Current state of the map for recursion + * @param PackageInterface $package The package to analyze * * @return array Map of package names to packages */ - private function collectDependencies(RepositorySet $repositorySet, array $collected, PackageInterface $package) + private function collectDependencies(RepositoryInterface $localRepos, array $collected, PackageInterface $package) { $requires = array_merge( $package->getRequires(), @@ -390,10 +388,11 @@ class PluginManager ); foreach ($requires as $requireLink) { - $requiredPackage = $this->lookupInstalledPackage($repositorySet, $requireLink); - if ($requiredPackage && !isset($collected[$requiredPackage->getName()])) { - $collected[$requiredPackage->getName()] = $requiredPackage; - $collected = $this->collectDependencies($repositorySet, $collected, $requiredPackage); + foreach ($this->lookupInstalledPackages($localRepos, $requireLink) as $requiredPackage) { + if (!isset($collected[$requiredPackage->getName()])) { + $collected[$requiredPackage->getName()] = $requiredPackage; + $collected = $this->collectDependencies($localRepos, $collected, $requiredPackage); + } } } @@ -405,16 +404,23 @@ class PluginManager * * Since dependencies are already installed this should always find one. * - * @param RepositorySet $repositorySet Repository set of installed packages only + * @param RepositoryInterface $localRepos Set of local repos * @param Link $link Package link to look up * - * @return PackageInterface|null The found package + * @return PackageInterface[] The found packages */ - private function lookupInstalledPackage(RepositorySet $repositorySet, Link $link) + private function lookupInstalledPackages(RepositoryInterface $localRepos, Link $link) { - $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint(), RepositorySet::ALLOW_PROVIDERS_REPLACERS | RepositorySet::ALLOW_SHADOWED_REPOSITORIES); + $matches = array(); + foreach ($localRepos->getPackages() as $candidate) { + if (in_array($link->getTarget(), $candidate->getNames(), true)) { + if ($link->getConstraint() === null || $link->getConstraint()->matches(new Constraint('=', $candidate->getVersion()))) { + $matches[] = $candidate; + } + } + } - return !empty($packages) ? $packages[0] : null; + return $matches; } /** diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index c08991ca7..b73907b48 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -134,7 +134,7 @@ class PluginInstallerTest extends TestCase public function testInstallNewPlugin() { $this->repository - ->expects($this->once()) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array())); $installer = new PluginInstaller($this->io, $this->composer); @@ -150,7 +150,7 @@ class PluginInstallerTest extends TestCase public function testInstallMultiplePlugins() { $this->repository - ->expects($this->once()) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[3]))); $installer = new PluginInstaller($this->io, $this->composer); @@ -169,7 +169,7 @@ class PluginInstallerTest extends TestCase public function testUpgradeWithNewClassName() { $this->repository - ->expects($this->once()) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[0]))); $this->repository @@ -190,7 +190,7 @@ class PluginInstallerTest extends TestCase public function testUninstall() { $this->repository - ->expects($this->once()) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[0]))); $this->repository @@ -210,7 +210,7 @@ class PluginInstallerTest extends TestCase public function testUpgradeWithSameClassName() { $this->repository - ->expects($this->once()) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[1]))); $this->repository @@ -230,7 +230,7 @@ class PluginInstallerTest extends TestCase public function testRegisterPluginOnlyOneTime() { $this->repository - ->expects($this->once()) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array())); $installer = new PluginInstaller($this->io, $this->composer); @@ -330,7 +330,7 @@ class PluginInstallerTest extends TestCase public function testCommandProviderCapability() { $this->repository - ->expects($this->once()) + ->expects($this->any()) ->method('getPackages') ->will($this->returnValue(array($this->packages[7]))); $installer = new PluginInstaller($this->io, $this->composer); From 78885c556a93dac7929e04b6f3b1a840ba19e74b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 18:05:04 +0100 Subject: [PATCH 252/321] Add missing use statement --- src/Composer/Plugin/PluginManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index cc0b39da9..ab7662bc8 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -19,6 +19,7 @@ use Composer\Package\CompletePackage; use Composer\Package\Package; use Composer\Package\Version\VersionParser; use Composer\Repository\RepositoryInterface; +use Composer\Repository\CompositeRepository; use Composer\Package\PackageInterface; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; From 2d8a8ed7e3551de4b92c62e4b1c2b8bbc7508ac0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 21:44:24 +0100 Subject: [PATCH 253/321] Add InstalledRepository to clean up some concepts and usages, remove BaseRepository --- .../Command/BaseDependencyCommand.php | 23 +++---- src/Composer/Command/HomeCommand.php | 4 +- src/Composer/Command/ShowCommand.php | 64 +++++++++---------- src/Composer/Installer.php | 16 ++--- src/Composer/Plugin/PluginManager.php | 44 +++---------- src/Composer/Repository/ArrayRepository.php | 11 ++-- .../Repository/CompositeRepository.php | 2 +- ...Repository.php => InstalledRepository.php} | 62 ++++++++++++++++-- src/Composer/Repository/RepositorySet.php | 20 +++--- .../Repository/InstalledRepositoryTest.php | 51 +++++++++++++++ 10 files changed, 184 insertions(+), 113 deletions(-) rename src/Composer/Repository/{BaseRepository.php => InstalledRepository.php} (80%) create mode 100644 tests/Composer/Test/Repository/InstalledRepositoryTest.php diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index dfacb0639..31975b5ac 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -14,8 +14,10 @@ namespace Composer\Command; use Composer\Package\Link; use Composer\Package\PackageInterface; -use Composer\Repository\ArrayRepository; +use Composer\Repository\InstalledArrayRepository; use Composer\Repository\CompositeRepository; +use Composer\Repository\RootPackageRepository; +use Composer\Repository\InstalledRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Plugin\CommandEvent; @@ -70,10 +72,9 @@ class BaseDependencyCommand extends BaseCommand $commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); - // Prepare repositories and set up a repo set $platformOverrides = $composer->getConfig()->get('platform') ?: array(); - $repository = new CompositeRepository(array( - new ArrayRepository(array($composer->getPackage())), + $installedRepo = new InstalledRepository(array( + new RootPackageRepository($composer->getPackage()), $composer->getRepositoryManager()->getLocalRepository(), new PlatformRepository(array(), $platformOverrides), )); @@ -84,25 +85,19 @@ class BaseDependencyCommand extends BaseCommand 2, $input->getArgument(self::ARGUMENT_CONSTRAINT) ); - $needle = strtolower($needle); // Find packages that are or provide the requested package first - $packages = array(); - foreach ($repository->getPackages() as $package) { - if (in_array($needle, $package->getNames(), true)) { - $packages[] = $package; - } - } + $packages = $installedRepo->findPackagesWithReplacersAndProviders($needle); if (empty($packages)) { throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } // If the version we ask for is not installed then we need to locate it in remote repos and add it. // This is needed for why-not to resolve conflicts from an uninstalled version against installed packages. - if (!$repository->findPackage($needle, $textConstraint)) { + if (!$installedRepo->findPackage($needle, $textConstraint)) { $defaultRepos = new CompositeRepository(RepositoryFactory::defaultRepos($this->getIO())); if ($match = $defaultRepos->findPackage($needle, $textConstraint)) { - $repository->addRepository(new ArrayRepository(array(clone $match))); + $installedRepo->addRepository(new InstalledArrayRepository(array(clone $match))); } } @@ -129,7 +124,7 @@ class BaseDependencyCommand extends BaseCommand $recursive = $renderTree || $input->getOption(self::OPTION_RECURSIVE); // Resolve dependencies - $results = $repository->getDependents($needles, $constraint, $inverted, $recursive); + $results = $installedRepo->getDependents($needles, $constraint, $inverted, $recursive); if (empty($results)) { $extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : ''; $this->getIO()->writeError(sprintf( diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php index b7d907066..8e43f39a4 100644 --- a/src/Composer/Command/HomeCommand.php +++ b/src/Composer/Command/HomeCommand.php @@ -14,7 +14,7 @@ namespace Composer\Command; use Composer\Package\CompletePackageInterface; use Composer\Repository\RepositoryInterface; -use Composer\Repository\ArrayRepository; +use Composer\Repository\RootPackageRepository; use Composer\Repository\RepositoryFactory; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; @@ -157,7 +157,7 @@ EOT if ($composer) { return array_merge( - array(new ArrayRepository(array($composer->getPackage()))), // root package + array(new RootPackageRepository($composer->getPackage())), // root package array($composer->getRepositoryManager()->getLocalRepository()), // installed packages $composer->getRepositoryManager()->getRepositories() // remotes ); diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 3dd75c697..125dd741a 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -22,13 +22,14 @@ use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; -use Composer\Repository\ArrayRepository; use Composer\Repository\ComposerRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; +use Composer\Repository\InstalledRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositorySet; +use Composer\Repository\RootPackageRepository; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; @@ -152,11 +153,11 @@ EOT if ($input->getOption('self')) { $package = $this->getComposer()->getPackage(); - $repos = $installedRepo = new ArrayRepository(array($package)); + $repos = $installedRepo = new InstalledRepository(array(new RootPackageRepository($package))); } elseif ($input->getOption('platform')) { - $repos = $installedRepo = $platformRepo; + $repos = $installedRepo = new InstalledRepository(array($platformRepo)); } elseif ($input->getOption('available')) { - $installedRepo = $platformRepo; + $installedRepo = new InstalledRepository(array($platformRepo)); if ($composer) { $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); } else { @@ -166,15 +167,15 @@ EOT } } elseif ($input->getOption('all') && $composer) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); - $installedRepo = new CompositeRepository(array($localRepo, $platformRepo)); + $installedRepo = new InstalledRepository(array($localRepo, $platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories())); } elseif ($input->getOption('all')) { $defaultRepos = RepositoryFactory::defaultRepos($io); $io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); - $installedRepo = $platformRepo; + $installedRepo = new InstalledRepository(array($platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } else { - $repos = $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); + $repos = $installedRepo = new InstalledRepository(array($this->getComposer()->getRepositoryManager()->getLocalRepository())); $rootPkg = $this->getComposer()->getPackage(); if (!$installedRepo->getPackages() && ($rootPkg->getRequires() || $rootPkg->getDevRequires())) { $io->writeError('No dependencies installed. Try running composer install or update.'); @@ -313,10 +314,7 @@ EOT foreach ($repos as $repo) { if ($repo === $platformRepo) { $type = 'platform'; - } elseif ( - $repo === $installedRepo - || ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true)) - ) { + } elseif ($repo === $installedRepo || in_array($repo, $installedRepo->getRepositories(), true)) { $type = 'installed'; } else { $type = 'available'; @@ -528,14 +526,14 @@ EOT /** * finds a package by name and version if provided * - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo * @param RepositoryInterface $repos * @param string $name * @param ConstraintInterface|string $version * @throws \InvalidArgumentException * @return array array(CompletePackageInterface, array of versions) */ - protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null) + protected function getPackage(InstalledRepository $installedRepo, RepositoryInterface $repos, $name, $version = null) { $name = strtolower($name); $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version; @@ -573,10 +571,10 @@ EOT * * @param CompletePackageInterface $package * @param array $versions - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo * @param PackageInterface|null $latestPackage */ - protected function printPackageInfo(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null) + protected function printPackageInfo(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $io = $this->getIO(); @@ -601,10 +599,10 @@ EOT * * @param CompletePackageInterface $package * @param array $versions - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo * @param PackageInterface|null $latestPackage */ - protected function printMeta(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null) + protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $io = $this->getIO(); $io->write('name : ' . $package->getPrettyName()); @@ -673,9 +671,9 @@ EOT * * @param CompletePackageInterface $package * @param array $versions - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo */ - protected function printVersions(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo) + protected function printVersions(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo) { uasort($versions, 'version_compare'); $versions = array_keys(array_reverse($versions)); @@ -749,10 +747,10 @@ EOT * * @param CompletePackageInterface $package * @param array $versions - * @param RepositoryInterface $installedRepo + * @param InstalledRepository $installedRepo * @param PackageInterface|null $latestPackage */ - protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null) + protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $json = array( 'name' => $package->getPrettyName(), @@ -972,15 +970,15 @@ EOT /** * Generate the package tree * - * @param PackageInterface $package - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $distantRepos + * @param PackageInterface $package + * @param InstalledRepository $installedRepo + * @param RepositoryInterface $remoteRepos * @return array */ protected function generatePackageTree( PackageInterface $package, - RepositoryInterface $installedRepo, - RepositoryInterface $distantRepos + InstalledRepository $installedRepo, + RepositoryInterface $remoteRepos ) { $requires = $package->getRequires(); ksort($requires); @@ -993,7 +991,7 @@ EOT 'version' => $require->getPrettyConstraint(), ); - $deepChildren = $this->addTree($requireName, $require, $installedRepo, $distantRepos, $packagesInTree); + $deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $packagesInTree); if ($deepChildren) { $treeChildDesc['requires'] = $deepChildren; @@ -1072,22 +1070,22 @@ EOT * * @param string $name * @param PackageInterface|string $package - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $distantRepos + * @param InstalledRepository $installedRepo + * @param RepositoryInterface $remoteRepos * @param array $packagesInTree * @return array */ protected function addTree( $name, $package, - RepositoryInterface $installedRepo, - RepositoryInterface $distantRepos, + InstalledRepository $installedRepo, + RepositoryInterface $remoteRepos, array $packagesInTree ) { $children = array(); list($package, $versions) = $this->getPackage( $installedRepo, - $distantRepos, + $remoteRepos, $name, $package->getPrettyConstraint() === 'self.version' ? $package->getConstraint() : $package->getPrettyConstraint() ); @@ -1104,7 +1102,7 @@ EOT if (!in_array($requireName, $currentTree, true)) { $currentTree[] = $requireName; - $deepChildren = $this->addTree($requireName, $require, $installedRepo, $distantRepos, $currentTree); + $deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $currentTree); if ($deepChildren) { $treeChildDesc['requires'] = $deepChildren; } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 46b7ea1ea..bdee1e9f0 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -51,6 +51,7 @@ use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\InstalledArrayRepository; +use Composer\Repository\InstalledRepository; use Composer\Repository\RootPackageRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; @@ -257,12 +258,12 @@ class Installer } if ($this->update) { - $installedRepos = array( + $installedRepo = new InstalledRepository(array( $this->locker->getLockedRepository($this->devMode), $this->createPlatformRepo(false), new RootPackageRepository(clone $this->package), - ); - $this->suggestedPackagesReporter->outputMinimalistic(new CompositeRepository($installedRepos)); + )); + $this->suggestedPackagesReporter->outputMinimalistic($installedRepo); } // Find abandoned packages and warn user @@ -859,8 +860,7 @@ class Installer } } - $repositorySet = new RepositorySet('dev'); - $repositorySet->addRepository($lockRepo); + $installedRepo = new InstalledRepository(array($lockRepo)); $seen = array(); @@ -870,7 +870,7 @@ class Installer $packageQueue = new \SplQueue; $nameMatchesRequiredPackage = false; - $depPackages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); + $depPackages = $installedRepo->findPackagesWithReplacersAndProviders($packageName); $matchesByPattern = array(); // check if the name is a glob pattern that did not match directly @@ -878,7 +878,7 @@ class Installer // add any installed package matching the whitelisted name/pattern $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { - $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); + $matchesByPattern[] = $installedRepo->findPackages($installedPackage['name']); } // add root requirements which match the whitelisted name/pattern @@ -919,7 +919,7 @@ class Installer $requires = $package->getRequires(); foreach ($requires as $require) { - $requirePackages = $repositorySet->findPackages($require->getTarget(), null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); + $requirePackages = $installedRepo->findPackagesWithReplacersAndProviders($require->getTarget()); foreach ($requirePackages as $requirePackage) { if (isset($this->updateWhitelist[$requirePackage->getName()])) { diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index ab7662bc8..97757f4fa 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -19,7 +19,7 @@ use Composer\Package\CompletePackage; use Composer\Package\Package; use Composer\Package\Version\VersionParser; use Composer\Repository\RepositoryInterface; -use Composer\Repository\CompositeRepository; +use Composer\Repository\InstalledRepository; use Composer\Package\PackageInterface; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; @@ -158,13 +158,13 @@ class PluginManager $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $localRepos = $localRepo; + $installedRepo = new InstalledRepository(array($localRepo)); if ($globalRepo) { - $localRepos = new CompositeRepository(array($localRepos, $globalRepo)); + $installedRepo->addRepository($globalRepo); } $autoloadPackages = array($package->getName() => $package); - $autoloadPackages = $this->collectDependencies($localRepos, $autoloadPackages, $package); + $autoloadPackages = $this->collectDependencies($installedRepo, $autoloadPackages, $package); $generator = $this->composer->getAutoloadGenerator(); $autoloads = array(); @@ -375,13 +375,13 @@ class PluginManager /** * Recursively generates a map of package names to packages for all deps * - * @param RepositoryInterface $localRepos Set of local repos - * @param array $collected Current state of the map for recursion - * @param PackageInterface $package The package to analyze + * @param InstalledRepository $installedRepo Set of local repos + * @param array $collected Current state of the map for recursion + * @param PackageInterface $package The package to analyze * * @return array Map of package names to packages */ - private function collectDependencies(RepositoryInterface $localRepos, array $collected, PackageInterface $package) + private function collectDependencies(InstalledRepository $installedRepo, array $collected, PackageInterface $package) { $requires = array_merge( $package->getRequires(), @@ -389,10 +389,10 @@ class PluginManager ); foreach ($requires as $requireLink) { - foreach ($this->lookupInstalledPackages($localRepos, $requireLink) as $requiredPackage) { + foreach ($installedRepo->findPackagesWithReplacersAndProviders($requireLink->getTarget(), $requireLink->getConstraint()) as $requiredPackage) { if (!isset($collected[$requiredPackage->getName()])) { $collected[$requiredPackage->getName()] = $requiredPackage; - $collected = $this->collectDependencies($localRepos, $collected, $requiredPackage); + $collected = $this->collectDependencies($installedRepo, $collected, $requiredPackage); } } } @@ -400,30 +400,6 @@ class PluginManager return $collected; } - /** - * Resolves a package link to a package in the installed repo set - * - * Since dependencies are already installed this should always find one. - * - * @param RepositoryInterface $localRepos Set of local repos - * @param Link $link Package link to look up - * - * @return PackageInterface[] The found packages - */ - private function lookupInstalledPackages(RepositoryInterface $localRepos, Link $link) - { - $matches = array(); - foreach ($localRepos->getPackages() as $candidate) { - if (in_array($link->getTarget(), $candidate->getNames(), true)) { - if ($link->getConstraint() === null || $link->getConstraint()->matches(new Constraint('=', $candidate->getVersion()))) { - $matches[] = $candidate; - } - } - } - - return $matches; - } - /** * Retrieves the path a package is installed to. * diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index ab67d42d2..f63f80753 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -25,7 +25,7 @@ use Composer\Semver\Constraint\Constraint; * * @author Nils Adermann */ -class ArrayRepository extends BaseRepository +class ArrayRepository implements RepositoryInterface { /** @var PackageInterface[] */ protected $packages; @@ -44,7 +44,7 @@ class ArrayRepository extends BaseRepository public function getRepoName() { - return 'array repo (defining '.count($this->packages).' package'.(count($this->packages) > 1 ? 's' : '').')'; + return 'array repo (defining '.$this->count().' package'.($this->count() > 1 ? 's' : '').')'; } /** @@ -126,8 +126,7 @@ class ArrayRepository extends BaseRepository foreach ($this->getPackages() as $package) { if ($name === $package->getName()) { - $pkgConstraint = new Constraint('==', $package->getVersion()); - if (null === $constraint || $constraint->matches($pkgConstraint)) { + if (null === $constraint || $constraint->matches(new Constraint('==', $package->getVersion()))) { $packages[] = $package; } } @@ -250,6 +249,10 @@ class ArrayRepository extends BaseRepository */ public function count() { + if (null === $this->packages) { + $this->initialize(); + } + return count($this->packages); } diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index 806934b7d..5242da688 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -19,7 +19,7 @@ use Composer\Package\PackageInterface; * * @author Beau Simensen */ -class CompositeRepository extends BaseRepository +class CompositeRepository implements RepositoryInterface { /** * List of repositories diff --git a/src/Composer/Repository/BaseRepository.php b/src/Composer/Repository/InstalledRepository.php similarity index 80% rename from src/Composer/Repository/BaseRepository.php rename to src/Composer/Repository/InstalledRepository.php index 1274dbb43..42a8d3ce6 100644 --- a/src/Composer/Repository/BaseRepository.php +++ b/src/Composer/Repository/InstalledRepository.php @@ -12,19 +12,48 @@ namespace Composer\Repository; -use Composer\Package\AliasPackage; -use Composer\Package\RootPackageInterface; +use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; +use Composer\Package\AliasPackage; +use Composer\Package\RootPackageInterface; use Composer\Package\Link; + /** - * Common ancestor class for generic repository functionality. + * Installed repository is a composite of all installed repo types. * - * @author Niels Keurentjes + * The main use case is tagging a repo as an "installed" repository, and offering a way to get providers/replacers easily. + * + * Installed repos are LockArrayRepository, InstalledRepositoryInterface, RootPackageRepository and PlatformRepository + * + * @author Jordi Boggiano */ -abstract class BaseRepository implements RepositoryInterface +class InstalledRepository extends CompositeRepository { + public function findPackagesWithReplacersAndProviders($name, $constraint = null) + { + $name = strtolower($name); + + if (null !== $constraint && !$constraint instanceof ConstraintInterface) { + $versionParser = new VersionParser(); + $constraint = $versionParser->parseConstraints($constraint); + } + + $matches = array(); + foreach ($this->getRepositories() as $repo) { + foreach ($repo->getPackages() as $candidate) { + if (in_array($name, $candidate->getNames(), true)) { + if (null === $constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { + $matches[] = $candidate; + } + } + } + } + + return $matches; + } + /** * Returns a list of links causing the requested needle packages to be installed, as an associative array with the * dependent's name as key, and an array containing in order the PackageInterface and Link describing the relationship @@ -176,4 +205,27 @@ abstract class BaseRepository implements RepositoryInterface return $results; } + + public function getRepoName() + { + return 'installed repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->repositories)).')'; + } + + /** + * Add a repository. + * @param RepositoryInterface $repository + */ + public function addRepository(RepositoryInterface $repository) + { + if ( + $repository instanceof LockArrayRepository + || $repository instanceof InstalledRepositoryInterface + || $repository instanceof RootPackageRepository + || $repository instanceof PlatformRepository + ) { + return parent::addRepository($repository); + } + + throw new \LogicException('An InstalledRepository can not contain a repository of type '.get_class($repository).' ('.$repository->getRepoName().')'); + } } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 6cafaa6c2..98bc36bc6 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -30,18 +30,14 @@ use Composer\Package\Version\StabilityFilter; */ class RepositorySet { - /** - * Packages which replace/provide the given name might be returned as well even if they do not match the name exactly - */ - const ALLOW_PROVIDERS_REPLACERS = 1; /** * Packages are returned even though their stability does not match the required stability */ - const ALLOW_UNACCEPTABLE_STABILITIES = 2; + const ALLOW_UNACCEPTABLE_STABILITIES = 1; /** * Packages will be looked up in all repositories, even after they have been found in a higher prio one */ - const ALLOW_SHADOWED_REPOSITORIES = 4; + const ALLOW_SHADOWED_REPOSITORIES = 2; /** @var array */ private $rootAliases; @@ -127,7 +123,6 @@ class RepositorySet */ public function findPackages($name, ConstraintInterface $constraint = null, $flags = 0) { - $exactMatch = ($flags & self::ALLOW_PROVIDERS_REPLACERS) === 0; $ignoreStability = ($flags & self::ALLOW_UNACCEPTABLE_STABILITIES) !== 0; $loadFromAllRepos = ($flags & self::ALLOW_SHADOWED_REPOSITORIES) !== 0; @@ -152,13 +147,14 @@ class RepositorySet $candidates = $packages ? call_user_func_array('array_merge', $packages) : array(); + // when using loadPackages above (!$loadFromAllRepos) the repos already filter for stability so no need to do it again + if ($ignoreStability || !$loadFromAllRepos) { + return $candidates; + } + $result = array(); foreach ($candidates as $candidate) { - if ($exactMatch && $candidate->getName() !== $name) { - continue; - } - - if (!$ignoreStability && $this->isPackageAcceptable($candidate->getNames(), $candidate->getStability())) { + if ($this->isPackageAcceptable($candidate->getNames(), $candidate->getStability())) { $result[] = $candidate; } } diff --git a/tests/Composer/Test/Repository/InstalledRepositoryTest.php b/tests/Composer/Test/Repository/InstalledRepositoryTest.php new file mode 100644 index 000000000..a37adb058 --- /dev/null +++ b/tests/Composer/Test/Repository/InstalledRepositoryTest.php @@ -0,0 +1,51 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Repository; + +use Composer\Repository\InstalledRepository; +use Composer\Repository\ArrayRepository; +use Composer\Repository\InstalledArrayRepository; +use Composer\Package\Link; +use Composer\Test\TestCase; + +class InstalledRepositoryTest extends TestCase +{ + public function testFindPackagesWithReplacersAndProviders() + { + $arrayRepoOne = new InstalledArrayRepository; + $arrayRepoOne->addPackage($foo = $this->getPackage('foo', '1')); + $arrayRepoOne->addPackage($foo2 = $this->getPackage('foo', '2')); + + $arrayRepoTwo = new InstalledArrayRepository; + $arrayRepoTwo->addPackage($bar = $this->getPackage('bar', '1')); + $arrayRepoTwo->addPackage($bar2 = $this->getPackage('bar', '2')); + + $foo->setReplaces(array(new Link('foo', 'provided'))); + $bar2->setProvides(array(new Link('bar', 'provided'))); + + $repo = new InstalledRepository(array($arrayRepoOne, $arrayRepoTwo)); + + $this->assertEquals(array($foo2), $repo->findPackagesWithReplacersAndProviders('foo', '2')); + $this->assertEquals(array($bar), $repo->findPackagesWithReplacersAndProviders('bar', '1')); + $this->assertEquals(array($foo, $bar2), $repo->findPackagesWithReplacersAndProviders('provided')); + } + + public function testAddRepository() + { + $arrayRepoOne = new ArrayRepository; + + $this->setExpectedException('LogicException'); + + new InstalledRepository(array($arrayRepoOne)); + } +} From 71005091f0cc85c167a636a8697ee57617c44556 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 13 Feb 2020 22:19:32 +0100 Subject: [PATCH 254/321] Fix invalid access to private prop --- src/Composer/Repository/InstalledRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/InstalledRepository.php b/src/Composer/Repository/InstalledRepository.php index 42a8d3ce6..13cd572f8 100644 --- a/src/Composer/Repository/InstalledRepository.php +++ b/src/Composer/Repository/InstalledRepository.php @@ -208,7 +208,7 @@ class InstalledRepository extends CompositeRepository public function getRepoName() { - return 'installed repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->repositories)).')'; + return 'installed repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->getRepositories())).')'; } /** From 0ad322e51f7f555d6a39ea0136ea4c946db64cf6 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 14 Feb 2020 16:55:21 +0100 Subject: [PATCH 255/321] Fix tests --- src/Composer/Repository/Vcs/GitHubDriver.php | 2 +- .../Fixtures/installer/install-funding-notice.test | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 9899fa76a..ef33a55b1 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -196,7 +196,7 @@ class GitHubDriver extends VcsDriver $graphql = 'query{repository(owner:"'.$this->owner.'",name:"'.$this->repository.'"){fundingLinks{platform,url}}}'; try { - $result = $this->remoteFilesystem->getContents($this->originUrl, 'https://api.github.com/graphql', false, array( + $result = $this->httpDownloader->get('https://api.github.com/graphql', array( 'http' => array( 'method' => 'POST', 'content' => json_encode(array('query' => $graphql)), diff --git a/tests/Composer/Test/Fixtures/installer/install-funding-notice.test b/tests/Composer/Test/Fixtures/installer/install-funding-notice.test index 638a31d97..bfac55518 100644 --- a/tests/Composer/Test/Fixtures/installer/install-funding-notice.test +++ b/tests/Composer/Test/Fixtures/installer/install-funding-notice.test @@ -41,10 +41,16 @@ Installs a simple package with exact match requirement --RUN-- install --EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 3 installs, 0 updates, 0 removals +Updating dependencies +Lock file operations: 3 installs, 0 updates, 0 removals + - Locking b/b (1.0.0) + - Locking d/d (1.0.0) + - Locking a/a (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 3 installs, 0 updates, 0 removals Generating autoload files 2 packages you are using are looking for funding. Use the composer fund command to find out more! From 20e4cc3b6f4f36745c3eb18de1664f8e04083e14 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 17 Feb 2020 11:23:15 +0100 Subject: [PATCH 256/321] Fix finding replacers/providers to check constraints correctly --- src/Composer/Repository/InstalledRepository.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Composer/Repository/InstalledRepository.php b/src/Composer/Repository/InstalledRepository.php index 13cd572f8..3d305ae39 100644 --- a/src/Composer/Repository/InstalledRepository.php +++ b/src/Composer/Repository/InstalledRepository.php @@ -44,8 +44,19 @@ class InstalledRepository extends CompositeRepository foreach ($this->getRepositories() as $repo) { foreach ($repo->getPackages() as $candidate) { if (in_array($name, $candidate->getNames(), true)) { - if (null === $constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { + if (null === $constraint) { $matches[] = $candidate; + continue; + } + if ($name === $candidate->getName() && $constraint->matches(new Constraint('==', $candidate->getVersion()))) { + $matches[] = $candidate; + continue; + } + foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { + if ($name === $link->getTarget() && ($link->getConstraint() === null || $constraint->matches($link->getConstraint()))) { + $matches[] = $candidate; + continue; + } } } } From 5b41b78809008d696720b2085fa32317ae2256cb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 18 Feb 2020 08:10:54 +0100 Subject: [PATCH 257/321] Optimize findPackagesWithReplacersAndProviders to avoid multiple loops over replace/provide links --- .../Repository/InstalledRepository.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Composer/Repository/InstalledRepository.php b/src/Composer/Repository/InstalledRepository.php index 3d305ae39..4f856e93b 100644 --- a/src/Composer/Repository/InstalledRepository.php +++ b/src/Composer/Repository/InstalledRepository.php @@ -43,21 +43,21 @@ class InstalledRepository extends CompositeRepository $matches = array(); foreach ($this->getRepositories() as $repo) { foreach ($repo->getPackages() as $candidate) { - if (in_array($name, $candidate->getNames(), true)) { - if (null === $constraint) { + if ($name === $candidate->getName()) { + if (null === $constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { + $matches[] = $candidate; + } + continue; + } + + foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { + if ( + $name === $link->getTarget() + && ($constraint === null || $link->getConstraint() === null || $constraint->matches($link->getConstraint())) + ) { $matches[] = $candidate; continue; } - if ($name === $candidate->getName() && $constraint->matches(new Constraint('==', $candidate->getVersion()))) { - $matches[] = $candidate; - continue; - } - foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { - if ($name === $link->getTarget() && ($link->getConstraint() === null || $constraint->matches($link->getConstraint()))) { - $matches[] = $candidate; - continue; - } - } } } } From a2eb0bab12af8c963553da288b641dfa6673780c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 18 Feb 2020 08:28:35 +0100 Subject: [PATCH 258/321] Abort loop correctly when package is a match --- src/Composer/Repository/InstalledRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/InstalledRepository.php b/src/Composer/Repository/InstalledRepository.php index 4f856e93b..49640fb90 100644 --- a/src/Composer/Repository/InstalledRepository.php +++ b/src/Composer/Repository/InstalledRepository.php @@ -56,7 +56,7 @@ class InstalledRepository extends CompositeRepository && ($constraint === null || $link->getConstraint() === null || $constraint->matches($link->getConstraint())) ) { $matches[] = $candidate; - continue; + continue 2; } } } From 2445ef5e1f00d9b1c23c0a6982690fca72260ca2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 10 Mar 2020 14:53:42 +0100 Subject: [PATCH 259/321] Refactor check-platform-reqs to use InstalledRepository and look for providers as well as platform packages, fixes #8645, closes #8676 Co-authored-by: Ellis Co-authored-by: Jordi Boggiano --- .../Command/CheckPlatformReqsCommand.php | 103 ++++++++++-------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/src/Composer/Command/CheckPlatformReqsCommand.php b/src/Composer/Command/CheckPlatformReqsCommand.php index 195a2c490..eba6d0946 100644 --- a/src/Composer/Command/CheckPlatformReqsCommand.php +++ b/src/Composer/Command/CheckPlatformReqsCommand.php @@ -20,6 +20,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\PlatformRepository; +use Composer\Repository\InstalledRepository; class CheckPlatformReqsCommand extends BaseCommand { @@ -48,12 +49,13 @@ EOT $requires = $composer->getPackage()->getRequires(); if ($input->getOption('no-dev')) { - $dependencies = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev'))->getPackages(); + $installedRepo = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev')); + $dependencies = $installedRepo->getPackages(); } else { - $dependencies = $composer->getRepositoryManager()->getLocalRepository()->getPackages(); + $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); // fallback to lockfile if installed repo is empty - if (!$dependencies) { - $dependencies = $composer->getLocker()->getLockedRepository(true)->getPackages(); + if (!$installedRepo->getPackages()) { + $installedRepo = $composer->getLocker()->getLockedRepository(true); } $requires += $composer->getPackage()->getDevRequires(); } @@ -61,7 +63,8 @@ EOT $requires[$require] = array($link); } - foreach ($dependencies as $package) { + $installedRepo = new InstalledRepository(array($installedRepo)); + foreach ($installedRepo->getPackages() as $package) { foreach ($package->getRequires() as $require => $link) { $requires[$require][] = $link; } @@ -69,19 +72,9 @@ EOT ksort($requires); - $platformRepo = new PlatformRepository(array(), array()); - $currentPlatformPackages = $platformRepo->getPackages(); - $currentPlatformPackageMap = array(); - - /** - * @var PackageInterface $currentPlatformPackage - */ - foreach ($currentPlatformPackages as $currentPlatformPackage) { - $currentPlatformPackageMap[$currentPlatformPackage->getName()] = $currentPlatformPackage; - } + $installedRepo->addRepository(new PlatformRepository(array(), array())); $results = array(); - $exitCode = 0; /** @@ -89,42 +82,62 @@ EOT */ foreach ($requires as $require => $links) { if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $require)) { - if (isset($currentPlatformPackageMap[$require])) { - $pass = true; - $version = $currentPlatformPackageMap[$require]->getVersion(); - - foreach ($links as $link) { - if (!$link->getConstraint()->matches(new Constraint('=', $version))) { - $results[] = array( - $currentPlatformPackageMap[$require]->getPrettyName(), - $currentPlatformPackageMap[$require]->getPrettyVersion(), - $link, - 'failed', - ); - $pass = false; - - $exitCode = max($exitCode, 1); + $candidates = $installedRepo->findPackagesWithReplacersAndProviders($require); + if ($candidates) { + $reqResults = array(); + foreach ($candidates as $candidate) { + if ($candidate->getName() === $require) { + $candidateConstraint = new Constraint('=', $candidate->getVersion()); + $candidateConstraint->setPrettyString($candidate->getPrettyVersion()); + } else { + foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { + if ($link->getTarget() === $require) { + $candidateConstraint = $link->getConstraint(); + break; + } + } + } + + foreach ($links as $link) { + if (!$link->getConstraint()->matches($candidateConstraint)) { + $reqResults[] = array( + $candidate->getName() === $require ? $candidate->getPrettyName() : $require, + $candidateConstraint->getPrettyString(), + $link, + 'failed'.($candidate->getName() === $require ? '' : ' provided by '.$candidate->getPrettyName().''), + ); + + // skip to next candidate + continue 2; + } } - } - if ($pass) { $results[] = array( - $currentPlatformPackageMap[$require]->getPrettyName(), - $currentPlatformPackageMap[$require]->getPrettyVersion(), + $candidate->getName() === $require ? $candidate->getPrettyName() : $require, + $candidateConstraint->getPrettyString(), null, - 'success', + 'success'.($candidate->getName() === $require ? '' : ' provided by '.$candidate->getPrettyName().''), ); - } - } else { - $results[] = array( - $require, - 'n/a', - $links[0], - 'missing', - ); - $exitCode = max($exitCode, 2); + // candidate matched, skip to next requirement + continue 2; + } + + // show the first error from every failed candidate + $results = array_merge($results, $reqResults); + $exitCode = max($exitCode, 1); + + continue; } + + $results[] = array( + $require, + 'n/a', + $links[0], + 'missing', + ); + + $exitCode = max($exitCode, 2); } } From 94e6cfc521eeae0548f1356c29b41da65c488441 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 10 Mar 2020 15:09:28 +0100 Subject: [PATCH 260/321] Only show direct dependencies suggestions by default, add --all flag to see all in suggest command, fixes #8660 --- doc/03-cli.md | 2 ++ src/Composer/Command/SuggestsCommand.php | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/doc/03-cli.md b/doc/03-cli.md index 0ee57aaaf..e0ae7487a 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -416,6 +416,8 @@ If you only want a list of suggested package names, use `--list`. * **--by-package:** Groups output by suggesting package (default). * **--by-suggestion:** Groups output by suggested package. +* **--all:** Show suggestions from all dependencies, including transitive ones (by + default only direct dependencies' suggestions are shown). * **--list:** Show only list of suggested package names. * **--no-dev:** Excludes suggestions from `require-dev` packages. diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 93c0e1e30..61875ba0b 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -31,6 +31,7 @@ class SuggestsCommand extends BaseCommand ->setDefinition(array( new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package (default)'), new InputOption('by-suggestion', null, InputOption::VALUE_NONE, 'Groups output by suggested package'), + new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show suggestions from all dependencies, including transitive ones'), new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'), @@ -70,6 +71,11 @@ EOT $reporter = new SuggestedPackagesReporter($this->getIO()); $filter = $input->getArgument('packages'); + if (empty($filter) && !$input->getOption('all')) { + $filter = array_map(function ($link) { + return $link->getTarget(); + }, array_merge($composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires())); + } foreach ($installedRepo->getPackages() as $package) { if (!empty($filter) && !in_array($package->getName(), $filter)) { continue; From 24a5a0dacb7ccec131bce5c4c0289da4e90a5479 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 10 Mar 2020 15:40:45 +0100 Subject: [PATCH 261/321] Skip invalid PSR-0/4 classes from optimized autoloader, fixes #8403 --- src/Composer/Autoload/ClassMapGenerator.php | 17 ++++++----------- .../Test/Autoload/AutoloadGeneratorTest.php | 2 -- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index 09bbd447f..7a19b8345 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -110,8 +110,7 @@ class ClassMapGenerator foreach ($classes as $class) { // skip classes not within the given namespace prefix - // TODO enable in Composer v1.11 or 2.0 whichever comes first - if (/* null === $autoloadType && */ null !== $namespace && '' !== $namespace && 0 !== strpos($class, $namespace)) { + if (null === $autoloadType && null !== $namespace && '' !== $namespace && 0 !== strpos($class, $namespace)) { continue; } @@ -180,19 +179,15 @@ class ClassMapGenerator // warn only if no valid classes, else silently skip invalid if (empty($validClasses)) { foreach ($rejectedClasses as $class) { - trigger_error( - "Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. It will not autoload anymore in Composer v2.0.", - E_USER_DEPRECATED - ); + if ($io) { + $io->writeError("Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. Skipping."); + } } - // TODO enable in Composer 2.0 - //return array(); + return array(); } - // TODO enable in Composer 2.0 & unskip test in AutoloadGeneratorTest::testPSRToClassMapIgnoresNonPSRClasses - //return $validClasses; - return $classes; + return $validClasses; } /** diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 3cd8f7ff2..6cb9b76dc 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -604,8 +604,6 @@ class AutoloadGeneratorTest extends TestCase { $package = new Package('a', '1.0', '1.0'); - $this->markTestSkipped('Skipped until ClassMapGenerator ignoring of invalid PSR-x classes is enabled'); - $package->setAutoload(array( 'psr-0' => array('psr0_' => 'psr0/'), 'psr-4' => array('psr4\\' => 'psr4/'), From c033644f8be083df6d72bf9a3d79dab2db22a704 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 10 Mar 2020 16:08:40 +0100 Subject: [PATCH 262/321] Present lock changes alphabetically, with uninstalls first, then all install/updates mixed --- src/Composer/Installer.php | 49 +++++++++++++------ .../installer/install-funding-notice.test | 2 +- .../Fixtures/installer/suggest-replaced.test | 2 +- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 71b0f8262..880840785 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -435,39 +435,60 @@ class Installer $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); + $installsUpdates = $uninstalls = array(); if ($lockTransaction->getOperations()) { - $installs = $updates = $uninstalls = array(); + $installNames = $updateNames = $uninstallNames = array(); foreach ($lockTransaction->getOperations() as $operation) { if ($operation instanceof InstallOperation) { - $installs[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); + $installsUpdates[] = $operation; + $installNames[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UpdateOperation) { - $updates[] = $operation->getTargetPackage()->getPrettyName().':'.$operation->getTargetPackage()->getFullPrettyVersion(); + $installsUpdates[] = $operation; + $updateNames[] = $operation->getTargetPackage()->getPrettyName().':'.$operation->getTargetPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UninstallOperation) { - $uninstalls[] = $operation->getPackage()->getPrettyName(); + $uninstalls[] = $operation; + $uninstallNames[] = $operation->getPackage()->getPrettyName(); } } $this->io->writeError(sprintf( "Lock file operations: %d install%s, %d update%s, %d removal%s", - count($installs), - 1 === count($installs) ? '' : 's', - count($updates), - 1 === count($updates) ? '' : 's', + count($installNames), + 1 === count($installNames) ? '' : 's', + count($updateNames), + 1 === count($updateNames) ? '' : 's', count($uninstalls), 1 === count($uninstalls) ? '' : 's' )); - if ($installs) { - $this->io->writeError("Installs: ".implode(', ', $installs), true, IOInterface::VERBOSE); + if ($installNames) { + $this->io->writeError("Installs: ".implode(', ', $installNames), true, IOInterface::VERBOSE); } - if ($updates) { - $this->io->writeError("Updates: ".implode(', ', $updates), true, IOInterface::VERBOSE); + if ($updateNames) { + $this->io->writeError("Updates: ".implode(', ', $updateNames), true, IOInterface::VERBOSE); } if ($uninstalls) { - $this->io->writeError("Removals: ".implode(', ', $uninstalls), true, IOInterface::VERBOSE); + $this->io->writeError("Removals: ".implode(', ', $uninstallNames), true, IOInterface::VERBOSE); } } - foreach ($lockTransaction->getOperations() as $operation) { + $sortByName = function ($a, $b) { + if ($a instanceof UpdateOperation) { + $a = $a->getTargetPackage()->getName(); + } else { + $a = $a->getPackage()->getName(); + } + if ($b instanceof UpdateOperation) { + $b = $b->getTargetPackage()->getName(); + } else { + $b = $b->getPackage()->getName(); + } + + return strcmp($a, $b); + }; + usort($uninstalls, $sortByName); + usort($installsUpdates, $sortByName); + + foreach (array_merge($uninstalls, $installsUpdates) as $operation) { // collect suggestions if ($operation instanceof InstallOperation) { $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); diff --git a/tests/Composer/Test/Fixtures/installer/install-funding-notice.test b/tests/Composer/Test/Fixtures/installer/install-funding-notice.test index bfac55518..7d10cc844 100644 --- a/tests/Composer/Test/Fixtures/installer/install-funding-notice.test +++ b/tests/Composer/Test/Fixtures/installer/install-funding-notice.test @@ -45,9 +45,9 @@ install Loading composer repositories with package information Updating dependencies Lock file operations: 3 installs, 0 updates, 0 removals + - Locking a/a (1.0.0) - Locking b/b (1.0.0) - Locking d/d (1.0.0) - - Locking a/a (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 3 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index e56af5093..a0e90332e 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -22,8 +22,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Locking c/c (1.0.0) - Locking a/a (1.0.0) + - Locking c/c (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals From 5c4ef1eb96d4b3aaed601a40870752460b94fa1a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 10 Mar 2020 16:57:34 +0100 Subject: [PATCH 263/321] Make package name validation a hard failure, fixes #7875 --- .../Package/Loader/RootPackageLoader.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 84e99a857..bf79d654e 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -72,10 +72,8 @@ class RootPackageLoader extends ArrayLoader { if (!isset($config['name'])) { $config['name'] = '__root__'; - } elseif ($this->io) { - if ($err = ValidatingArrayLoader::hasPackageNamingError($config['name'])) { - $this->io->writeError('Deprecation warning: Your package name '.$err.' Make sure you fix this as Composer 2.0 will error.'); - } + } elseif ($err = ValidatingArrayLoader::hasPackageNamingError($config['name'])) { + throw new \RuntimeException('Your package name '.$err); } $autoVersioned = false; if (!isset($config['version'])) { @@ -142,13 +140,11 @@ class RootPackageLoader extends ArrayLoader } } - if ($this->io) { - foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { - if (isset($config[$linkType])) { - foreach ($config[$linkType] as $linkName => $constraint) { - if ($err = ValidatingArrayLoader::hasPackageNamingError($linkName, true)) { - $this->io->writeError('Deprecation warning: '.$linkType.'.'.$err.' Make sure you fix this as Composer 2.0 will error.'); - } + foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { + if (isset($config[$linkType])) { + foreach ($config[$linkType] as $linkName => $constraint) { + if ($err = ValidatingArrayLoader::hasPackageNamingError($linkName, true)) { + throw new \RuntimeException($linkType.'.'.$err); } } } From ffac8646fc733396885baee2709c7b303bef10be Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 11 Mar 2020 09:34:40 +0100 Subject: [PATCH 264/321] Fix tests --- .../installer/circular-dependency2.test | 2 +- .../disjunctive-multi-constraints.test | 14 +-- .../installer/install-from-empty-lock.test | 12 +-- .../install-from-lock-removes-package.test | 28 +++--- .../plugins-are-installed-first.test | 28 +++--- .../installer/replace-priorities.test | 22 ++--- .../update-whitelist-locked-require.test | 42 ++++----- ...telist-patterns-with-all-dependencies.test | 66 +++++++------- ...-whitelist-patterns-with-dependencies.test | 74 +++++++-------- ...elist-patterns-with-root-dependencies.test | 90 +++++++++---------- ...itelist-patterns-without-dependencies.test | 62 ++++++------- .../installer/update-whitelist-patterns.test | 58 ++++++------ .../update-whitelist-removes-unused.test | 32 +++---- .../update-whitelist-with-dependencies.test | 52 +++++------ ...te-whitelist-with-dependency-conflict.test | 48 +++++----- .../Fixtures/installer/update-whitelist.test | 50 +++++------ .../Package/Loader/RootPackageLoaderTest.php | 8 +- 17 files changed, 344 insertions(+), 344 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/circular-dependency2.test b/tests/Composer/Test/Fixtures/installer/circular-dependency2.test index 6024c17f2..2a2cc4b0d 100644 --- a/tests/Composer/Test/Fixtures/installer/circular-dependency2.test +++ b/tests/Composer/Test/Fixtures/installer/circular-dependency2.test @@ -2,7 +2,7 @@ Circular dependencies are possible between packages --COMPOSER-- { - "name": "root", + "name": "root/pkg", "version": "dev-master", "require": { "require/itself": "1.0.0", diff --git a/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test b/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test index b274c5de2..0e130e747 100644 --- a/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test +++ b/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test @@ -6,19 +6,19 @@ Disjunctive multi constraints work { "type": "package", "package": [ - { "name": "foo", "version": "1.1.0" }, - { "name": "foo", "version": "1.0.0" }, - { "name": "bar", "version": "1.1.0", "require": { "foo": "1.0.*" } } + { "name": "foo/pkg", "version": "1.1.0" }, + { "name": "foo/pkg", "version": "1.0.0" }, + { "name": "bar/pkg", "version": "1.1.0", "require": { "foo/pkg": "1.0.*" } } ] } ], "require": { - "bar": "1.*", - "foo": "1.0.*|1.1.*" + "bar/pkg": "1.*", + "foo/pkg": "1.0.*|1.1.*" } } --RUN-- install --EXPECT-- -Installing foo (1.0.0) -Installing bar (1.1.0) +Installing foo/pkg (1.0.0) +Installing bar/pkg (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test index c3abd2377..0bba90cff 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test @@ -6,20 +6,20 @@ Requirements from the composer file are not installed if the lock file is presen { "type": "package", "package": [ - { "name": "required", "version": "1.0.0" }, - { "name": "newly-required", "version": "1.0.0" } + { "name": "required/pkg", "version": "1.0.0" }, + { "name": "newly-required/pkg", "version": "1.0.0" } ] } ], "require": { - "required": "1.0.0", - "newly-required": "1.0.0" + "required/pkg": "1.0.0", + "newly-required/pkg": "1.0.0" } } --LOCK-- { "packages": [ - { "name": "required", "version": "1.0.0" } + { "name": "required/pkg", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -31,4 +31,4 @@ Requirements from the composer file are not installed if the lock file is presen --RUN-- install --EXPECT-- -Installing required (1.0.0) +Installing required/pkg (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index d60cd7e94..b1677956c 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -6,24 +6,24 @@ Install from a lock file that deleted a package { "type": "package", "package": [ - { "name": "whitelisted", "version": "1.1.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "fixed-dependency": "1.0.0", "old-dependency": "1.0.0" } }, - { "name": "fixed-dependency", "version": "1.1.0" }, - { "name": "fixed-dependency", "version": "1.0.0" }, - { "name": "old-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.1.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.1.0" }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } ] } ], "require": { - "whitelisted": "1.*", - "fixed-dependency": "1.*" + "whitelisted/pkg": "1.*", + "fixed/dependency": "1.*" } } --LOCK-- { "packages": [ - { "name": "whitelisted", "version": "1.1.0" }, - { "name": "fixed-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.1.0" }, + { "name": "fixed/dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -33,12 +33,12 @@ Install from a lock file that deleted a package } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, - { "name": "fixed-dependency", "version": "1.0.0" }, - { "name": "old-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } ] --RUN-- install --EXPECT-- -Uninstalling old-dependency (1.0.0) -Upgrading whitelisted (1.0.0 => 1.1.0) +Uninstalling old/dependency (1.0.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test index 009eb576d..6a8b2030c 100644 --- a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test +++ b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test @@ -6,26 +6,26 @@ Composer installers and their requirements are installed first { "type": "package", "package": [ - { "name": "pkg", "version": "1.0.0" }, - { "name": "pkg2", "version": "1.0.0" }, - { "name": "inst", "version": "1.0.0", "type": "composer-plugin" }, - { "name": "inst-with-req", "version": "1.0.0", "type": "composer-plugin", "require": { "php": ">=5", "ext-json": "*", "composer-plugin-api": "*" } }, - { "name": "inst-with-req2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } } + { "name": "pkg/1", "version": "1.0.0" }, + { "name": "pkg/2", "version": "1.0.0" }, + { "name": "inst/pkg", "version": "1.0.0", "type": "composer-plugin" }, + { "name": "inst/with-req", "version": "1.0.0", "type": "composer-plugin", "require": { "php": ">=5", "ext-json": "*", "composer-plugin-api": "*" } }, + { "name": "inst/with-req2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg/2": "*" } } ] } ], "require": { - "pkg": "1.0.0", - "inst": "1.0.0", - "inst-with-req2": "1.0.0", - "inst-with-req": "1.0.0" + "pkg/1": "1.0.0", + "inst/pkg": "1.0.0", + "inst/with-req2": "1.0.0", + "inst/with-req": "1.0.0" } } --RUN-- install --EXPECT-- -Installing inst (1.0.0) -Installing inst-with-req (1.0.0) -Installing pkg2 (1.0.0) -Installing inst-with-req2 (1.0.0) -Installing pkg (1.0.0) +Installing inst/pkg (1.0.0) +Installing inst/with-req (1.0.0) +Installing pkg/2 (1.0.0) +Installing inst/with-req2 (1.0.0) +Installing pkg/1 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/replace-priorities.test b/tests/Composer/Test/Fixtures/installer/replace-priorities.test index d69dd9a22..e561b548b 100644 --- a/tests/Composer/Test/Fixtures/installer/replace-priorities.test +++ b/tests/Composer/Test/Fixtures/installer/replace-priorities.test @@ -6,28 +6,28 @@ Replace takes precedence only in higher priority repositories and if explicitly { "type": "package", "package": [ - { "name": "forked", "version": "1.1.0", "replace": { "package2": "1.1.0" } } + { "name": "forked/pkg", "version": "1.1.0", "replace": { "package/2": "1.1.0" } } ] }, { "type": "package", "package": [ - { "name": "package", "version": "1.0.0" }, - { "name": "package2", "version": "1.0.0" }, - { "name": "package3", "version": "1.0.0", "require": { "forked": "*" } }, - { "name": "hijacker", "version": "1.1.0", "replace": { "package": "1.1.0" } } + { "name": "package/1", "version": "1.0.0" }, + { "name": "package/2", "version": "1.0.0" }, + { "name": "package/3", "version": "1.0.0", "require": { "forked/pkg": "*" } }, + { "name": "hijacker/pkg", "version": "1.1.0", "replace": { "package/1": "1.1.0" } } ] } ], "require": { - "package": "1.*", - "package2": "1.*", - "package3": "1.*" + "package/1": "1.*", + "package/2": "1.*", + "package/3": "1.*" } } --RUN-- install --EXPECT-- -Installing package (1.0.0) -Installing forked (1.1.0) -Installing package3 (1.0.0) +Installing package/1 (1.0.0) +Installing forked/pkg (1.1.0) +Installing package/3 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test index 3fb16d52f..0f009ae6f 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test @@ -6,36 +6,36 @@ Update with a package whitelist only updates those packages if they are not pres { "type": "package", "package": [ - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0", "fixed-dependency": "1.*" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, - { "name": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "fixed-dependency", "version": "1.1.0", "require": { "fixed-sub-dependency": "1.*" } }, - { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, - { "name": "fixed-sub-dependency", "version": "1.1.0" }, - { "name": "fixed-sub-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "fixed/dependency": "1.*" } }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "fixed/dependency", "version": "1.1.0", "require": { "fixed/sub-dependency": "1.*" } }, + { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, + { "name": "fixed/sub-dependency", "version": "1.1.0" }, + { "name": "fixed/sub-dependency", "version": "1.0.0" } ] } ], "require": { - "whitelisted": "1.*", - "fixed-dependency": "1.*" + "whitelisted/pkg": "1.*", + "fixed/dependency": "1.*" } } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, - { "name": "fixed-sub-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, + { "name": "fixed/sub-dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, - { "name": "fixed-sub-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, + { "name": "fixed/sub-dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -47,7 +47,7 @@ Update with a package whitelist only updates those packages if they are not pres "platform-dev": [] } --RUN-- -update whitelisted dependency +update whitelisted/pkg dependency/pkg --EXPECT-- -Upgrading dependency (1.0.0 => 1.1.0) -Upgrading whitelisted (1.0.0 => 1.1.0) +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test index cbf350f0b..95fd639f2 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test @@ -6,47 +6,47 @@ Update with a package whitelist pattern and all-dependencies flag updates packag { "type": "package", "package": [ - { "name": "fixed", "version": "1.1.0" }, - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, - { "name": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] } ], "require": { - "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "dependency": "1.*", - "unrelated": "1.*" + "fixed/pkg": "1.*", + "whitelisted/pkg-component1": "1.*", + "whitelisted/pkg-component2": "1.*", + "dependency/pkg": "1.*", + "unrelated/pkg": "1.*" } } --INSTALLED-- [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -58,8 +58,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag "platform-dev": [] } --RUN-- -update whitelisted-* --with-all-dependencies +update whitelisted/pkg-* --with-all-dependencies --EXPECT-- -Upgrading whitelisted-component1 (1.0.0 => 1.1.0) -Upgrading dependency (1.0.0 => 1.1.0) -Upgrading whitelisted-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test index 68c1218c3..d40a924ab 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test @@ -6,51 +6,51 @@ Update with a package whitelist only updates those packages and their dependenci { "type": "package", "package": [ - { "name": "fixed", "version": "1.1.0" }, - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, - { "name": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "root-dependency", "version": "1.1.0" }, - { "name": "root-dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "root/pkg-dependency", "version": "1.1.0" }, + { "name": "root/pkg-dependency", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] } ], "require": { - "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "root-dependency": "1.*", - "unrelated": "1.*" + "fixed/pkg": "1.*", + "whitelisted/pkg-component1": "1.*", + "whitelisted/pkg-component2": "1.*", + "root/pkg-dependency": "1.*", + "unrelated/pkg": "1.*" } } --INSTALLED-- [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "root-dependency", "version": "1.0.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "root/pkg-dependency", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "root-dependency", "version": "1.0.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "root/pkg-dependency", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -60,8 +60,8 @@ Update with a package whitelist only updates those packages and their dependenci "prefer-lowest": false } --RUN-- -update whitelisted-* --with-dependencies +update whitelisted/pkg-* --with-dependencies --EXPECT-- -Upgrading whitelisted-component1 (1.0.0 => 1.1.0) -Upgrading dependency (1.0.0 => 1.1.0) -Upgrading whitelisted-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test index dab3d87e3..02f544577 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test @@ -6,59 +6,59 @@ Update with a package whitelist only updates those packages and their dependenci { "type": "package", "package": [ - { "name": "fixed", "version": "1.1.0" }, - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0", "require": { "whitelisted-component2": "1.1.0" } }, - { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.1.0", "whitelisted-component5": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "whitelisted-component3", "version": "1.1.0", "require": { "whitelisted-component4": "1.1.0" } }, - { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, - { "name": "whitelisted-component4", "version": "1.1.0" }, - { "name": "whitelisted-component4", "version": "1.0.0" }, - { "name": "whitelisted-component5", "version": "1.1.0" }, - { "name": "whitelisted-component5", "version": "1.0.0" }, - { "name": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.1.0", "require": { "whitelisted/pkg-component2": "1.1.0" } }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, + { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "whitelisted/pkg-component5": "1.0.0" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "whitelisted/pkg-component3", "version": "1.1.0", "require": { "whitelisted/pkg-component4": "1.1.0" } }, + { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, + { "name": "whitelisted/pkg-component4", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component5", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] } ], "require": { - "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "whitelisted-component3": "1.0.0", - "unrelated": "1.*" + "fixed/pkg": "1.*", + "whitelisted/pkg-component1": "1.*", + "whitelisted/pkg-component2": "1.*", + "whitelisted/pkg-component3": "1.0.0", + "unrelated/pkg": "1.*" } } --INSTALLED-- [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, - { "name": "whitelisted-component4", "version": "1.0.0" }, - { "name": "whitelisted-component5", "version": "1.0.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, + { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, - { "name": "whitelisted-component4", "version": "1.0.0" }, - { "name": "whitelisted-component5", "version": "1.0.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, + { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -70,8 +70,8 @@ Update with a package whitelist only updates those packages and their dependenci "platform-dev": [] } --RUN-- -update whitelisted-* --with-dependencies +update whitelisted/pkg-* --with-dependencies --EXPECT-- -Upgrading dependency (1.0.0 => 1.1.0) -Upgrading whitelisted-component2 (1.0.0 => 1.1.0) -Upgrading whitelisted-component1 (1.0.0 => 1.1.0) +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test index 337fe3a9b..6cd1d7778 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test @@ -6,46 +6,46 @@ Update with a package whitelist only updates those packages matching the pattern { "type": "package", "package": [ - { "name": "fixed", "version": "1.1.0" }, - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, - { "name": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] } ], "require": { - "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "unrelated": "1.*" + "fixed/pkg": "1.*", + "whitelisted/pkg-component1": "1.*", + "whitelisted/pkg-component2": "1.*", + "unrelated/pkg": "1.*" } } --INSTALLED-- [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, + { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -55,7 +55,7 @@ Update with a package whitelist only updates those packages matching the pattern "prefer-lowest": false } --RUN-- -update whitelisted-* +update whitelisted/pkg-* --EXPECT-- -Upgrading whitelisted-component1 (1.0.0 => 1.1.0) -Upgrading whitelisted-component2 (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test index 8e4fd2034..738f0af74 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test @@ -6,48 +6,48 @@ Update with a package whitelist only updates those corresponding to the pattern { "type": "package", "package": [ - { "name": "vendor/Test-Package", "version": "2.0" }, - { "name": "vendor/NotMe", "version": "2.0" }, - { "name": "exact/Test-Package", "version": "2.0" }, - { "name": "notexact/TestPackage", "version": "2.0" }, - { "name": "all/Package1", "version": "2.0" }, - { "name": "all/Package2", "version": "2.0" }, + { "name": "vendor/test-package", "version": "2.0" }, + { "name": "vendor/not-me", "version": "2.0" }, + { "name": "exact/test-package", "version": "2.0" }, + { "name": "notexact/testpackage", "version": "2.0" }, + { "name": "all/package1", "version": "2.0" }, + { "name": "all/package2", "version": "2.0" }, { "name": "another/another", "version": "2.0" }, { "name": "no/regexp", "version": "2.0" } ] } ], "require": { - "vendor/Test-Package": "*.*", - "vendor/NotMe": "*.*", - "exact/Test-Package": "*.*", - "notexact/TestPackage": "*.*", - "all/Package1": "*.*", - "all/Package2": "*.*", + "vendor/test-package": "*.*", + "vendor/not-me": "*.*", + "exact/test-package": "*.*", + "notexact/testpackage": "*.*", + "all/package1": "*.*", + "all/package2": "*.*", "another/another": "*.*", "no/regexp": "*.*" } } --INSTALLED-- [ - { "name": "vendor/Test-Package", "version": "1.0" }, - { "name": "vendor/NotMe", "version": "1.0" }, - { "name": "exact/Test-Package", "version": "1.0" }, - { "name": "notexact/TestPackage", "version": "1.0" }, - { "name": "all/Package1", "version": "1.0" }, - { "name": "all/Package2", "version": "1.0" }, + { "name": "vendor/test-package", "version": "1.0" }, + { "name": "vendor/not-me", "version": "1.0" }, + { "name": "exact/test-package", "version": "1.0" }, + { "name": "notexact/testpackage", "version": "1.0" }, + { "name": "all/package1", "version": "1.0" }, + { "name": "all/package2", "version": "1.0" }, { "name": "another/another", "version": "1.0" }, { "name": "no/regexp", "version": "1.0" } ] --LOCK-- { "packages": [ - { "name": "vendor/Test-Package", "version": "1.0" }, - { "name": "vendor/NotMe", "version": "1.0" }, - { "name": "exact/Test-Package", "version": "1.0" }, - { "name": "notexact/TestPackage", "version": "1.0" }, - { "name": "all/Package1", "version": "1.0" }, - { "name": "all/Package2", "version": "1.0" }, + { "name": "vendor/test-package", "version": "1.0" }, + { "name": "vendor/not-me", "version": "1.0" }, + { "name": "exact/test-package", "version": "1.0" }, + { "name": "notexact/testpackage", "version": "1.0" }, + { "name": "all/package1", "version": "1.0" }, + { "name": "all/package2", "version": "1.0" }, { "name": "another/another", "version": "1.0" }, { "name": "no/regexp", "version": "1.0" } ], @@ -61,9 +61,9 @@ Update with a package whitelist only updates those corresponding to the pattern "platform-dev": [] } --RUN-- -update vendor/Test* exact/Test-Package notexact/Test all/* no/reg?xp +update vendor/Test* exact/test-package notexact/Test all/* no/reg.?xp --EXPECT-- -Upgrading all/Package1 (1.0 => 2.0) -Upgrading all/Package2 (1.0 => 2.0) -Upgrading exact/Test-Package (1.0 => 2.0) -Upgrading vendor/Test-Package (1.0 => 2.0) +Upgrading all/package1 (1.0 => 2.0) +Upgrading all/package2 (1.0 => 2.0) +Upgrading exact/test-package (1.0 => 2.0) +Upgrading vendor/test-package (1.0 => 2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test index 7b720e3ca..0863d1321 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -6,31 +6,31 @@ Update with a package whitelist removes unused packages { "type": "package", "package": [ - { "name": "whitelisted", "version": "1.1.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "fixed-dependency": "1.0.0", "old-dependency": "1.0.0" } }, - { "name": "fixed-dependency", "version": "1.1.0" }, - { "name": "fixed-dependency", "version": "1.0.0" }, - { "name": "old-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.1.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.1.0" }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } ] } ], "require": { - "whitelisted": "1.*", - "fixed-dependency": "1.*" + "whitelisted/pkg": "1.*", + "fixed/dependency": "1.*" } } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, - { "name": "fixed-dependency", "version": "1.0.0" }, - { "name": "old-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, - { "name": "fixed-dependency", "version": "1.0.0" }, - { "name": "old-dependency", "version": "1.0.0" } + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "fixed/dependency", "version": "1.0.0" }, + { "name": "old/dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -42,7 +42,7 @@ Update with a package whitelist removes unused packages "platform-dev": [] } --RUN-- -update --with-dependencies whitelisted +update --with-dependencies whitelisted/pkg --EXPECT-- -Uninstalling old-dependency (1.0.0) -Upgrading whitelisted (1.0.0 => 1.1.0) +Uninstalling old/dependency (1.0.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test index 02e772337..079ad9d2b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test @@ -6,41 +6,41 @@ Update with a package whitelist only updates those packages and their dependenci { "type": "package", "package": [ - { "name": "fixed", "version": "1.1.0" }, - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] } ], "require": { - "fixed": "1.*", - "whitelisted": "1.*", - "unrelated": "1.*" + "fixed/pkg": "1.*", + "whitelisted/pkg": "1.*", + "unrelated/pkg": "1.*" } } --INSTALLED-- [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -50,7 +50,7 @@ Update with a package whitelist only updates those packages and their dependenci "prefer-lowest": false } --RUN-- -update whitelisted --with-dependencies +update whitelisted/pkg --with-dependencies --EXPECT-- -Upgrading dependency (1.0.0 => 1.1.0) -Upgrading whitelisted (1.0.0 => 1.1.0) +Upgrading dependency/pkg (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test index 38a7bbf54..299c505cb 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test @@ -6,41 +6,41 @@ Update with a package whitelist only updates whitelisted packages if no dependen { "type": "package", "package": [ - { "name": "fixed", "version": "1.1.0" }, - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] } ], "require": { - "fixed": "1.*", - "whitelisted": "1.*", - "unrelated": "1.*" + "fixed/pkg": "1.*", + "whitelisted/pkg": "1.*", + "unrelated/pkg": "1.*" } } --INSTALLED-- [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -50,5 +50,5 @@ Update with a package whitelist only updates whitelisted packages if no dependen "prefer-lowest": false } --RUN-- -update whitelisted +update whitelisted/pkg --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index 826f9fce1..a02e00c4b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -6,41 +6,41 @@ Update with a package whitelist only updates those packages listed as command ar { "type": "package", "package": [ - { "name": "fixed", "version": "1.1.0" }, - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, - { "name": "dependency", "version": "1.1.0" }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.1.0" }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.1.0" }, + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "dependency/pkg", "version": "1.1.0" }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.1.0" }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] } ], "require": { - "fixed": "1.*", - "whitelisted": "1.*", - "unrelated": "1.*" + "fixed/pkg": "1.*", + "whitelisted/pkg": "1.*", + "unrelated/pkg": "1.*" } } --INSTALLED-- [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, - { "name": "dependency", "version": "1.0.0" }, - { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, - { "name": "unrelated-dependency", "version": "1.0.0" } + { "name": "fixed/pkg", "version": "1.0.0" }, + { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, + { "name": "unrelated/pkg-dependency", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -52,6 +52,6 @@ Update with a package whitelist only updates those packages listed as command ar "platform-dev": [] } --RUN-- -update whitelisted +update whitelisted/pkg --EXPECT-- -Upgrading whitelisted (1.0.0 => 1.1.0) +Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php index a94539279..25d39270c 100644 --- a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php @@ -46,8 +46,8 @@ class RootPackageLoaderTest extends TestCase 'zux/complex' => '~1.0,>=1.0.2@dev', 'or/op' => '^2.0@dev || ^2.0@dev', 'multi/lowest-wins' => '^2.0@rc || >=3.0@dev , ~3.5@alpha', - 'or/op/without-flags' => 'dev-master || 2.0 , ~3.5-alpha', - 'or/op/without-flags2' => '3.0-beta || 2.0 , ~3.5-alpha', + 'or/op-without-flags' => 'dev-master || 2.0 , ~3.5-alpha', + 'or/op-without-flags2' => '3.0-beta || 2.0 , ~3.5-alpha', ), 'minimum-stability' => 'alpha', )); @@ -59,8 +59,8 @@ class RootPackageLoaderTest extends TestCase 'zux/complex' => BasePackage::STABILITY_DEV, 'or/op' => BasePackage::STABILITY_DEV, 'multi/lowest-wins' => BasePackage::STABILITY_DEV, - 'or/op/without-flags' => BasePackage::STABILITY_DEV, - 'or/op/without-flags2' => BasePackage::STABILITY_ALPHA, + 'or/op-without-flags' => BasePackage::STABILITY_DEV, + 'or/op-without-flags2' => BasePackage::STABILITY_ALPHA, ), $package->getStabilityFlags()); } From a7a975ec1c0e3f30a04ea9ad75b696382559bc19 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 11 Mar 2020 13:37:39 +0100 Subject: [PATCH 265/321] Alias dev-master/trunk/default to 9999999-dev instead of normalizing the version to that, fixes #8323 --- composer.json | 2 +- composer.lock | 109 +++++++++++++++--- src/Composer/Command/ShowCommand.php | 17 +-- src/Composer/DependencyResolver/Problem.php | 4 + src/Composer/Package/Loader/ArrayLoader.php | 60 +++++----- .../Package/Loader/ValidatingArrayLoader.php | 2 +- .../Package/Version/VersionParser.php | 3 + .../installer/alias-with-reference.test | 1 + .../aliased-priority-conflicting.test | 21 ++-- .../installer/install-aliased-alias.test | 1 + .../installer/install-dev-using-dist.test | 1 + .../Fixtures/installer/install-reference.test | 1 + ...ed-conflict-does-not-match-dev-master.test | 31 +++++ .../installer/update-changes-url.test | 1 + ...pdate-dev-to-new-ref-picks-up-changes.test | 1 + .../update-downgrades-unstable-packages.test | 1 + .../update-to-empty-from-locked.test | 1 + ...dating-dev-from-lock-removes-old-deps.test | 1 + .../Package/Loader/RootPackageLoaderTest.php | 2 + .../Loader/ValidatingArrayLoaderTest.php | 2 - .../Package/Version/VersionGuesserTest.php | 4 +- 21 files changed, 200 insertions(+), 66 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test diff --git a/composer.json b/composer.json index ba26cf7ec..2204a5639 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "require": { "php": "^5.3.2 || ^7.0", "composer/ca-bundle": "^1.0", - "composer/semver": "^1.0", + "composer/semver": "^2.0@dev", "composer/spdx-licenses": "^1.2", "composer/xdebug-handler": "^1.1", "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", diff --git a/composer.lock b/composer.lock index f77b6adc3..4b5a5e636 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7bfefa6f7d4d8c18836028dae680bd4f", + "content-hash": "a0a9399315ac0b612d4296b8df745112", "packages": [ { "name": "composer/ca-bundle", @@ -60,20 +60,25 @@ "ssl", "tls" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/master" + }, "time": "2020-01-13T10:02:55+00:00" }, { "name": "composer/semver", - "version": "1.5.1", + "version": "2.0.x-dev", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" + "reference": "4df5ff3249f01018504939d66040d8d2b783d820" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", + "url": "https://api.github.com/repos/composer/semver/zipball/4df5ff3249f01018504939d66040d8d2b783d820", + "reference": "4df5ff3249f01018504939d66040d8d2b783d820", "shasum": "" }, "require": { @@ -85,7 +90,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -121,7 +126,22 @@ "validation", "versioning" ], - "time": "2020-01-13T12:06:48+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/2.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-03-11T13:41:23+00:00" }, { "name": "composer/spdx-licenses", @@ -181,6 +201,11 @@ "spdx", "validator" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.3" + }, "time": "2020-02-14T07:44:31+00:00" }, { @@ -225,6 +250,11 @@ "Xdebug", "performance" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/master" + }, "funding": [ { "url": "https://packagist.com", @@ -388,12 +418,6 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com" - } - ], "description": "A lightweight implementation of CommonJS Promises/A for PHP", "time": "2016-03-07T13:46:50+00:00" }, @@ -492,6 +516,10 @@ "keywords": [ "phar" ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.1.0" + }, "time": "2020-02-14T15:25:33+00:00" }, { @@ -761,6 +789,16 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ @@ -769,6 +807,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/master" + }, "time": "2020-01-13T11:15:53+00:00" }, { @@ -828,6 +869,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/master" + }, "time": "2020-01-13T11:15:53+00:00" }, { @@ -983,6 +1027,16 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/2.x" + }, "time": "2016-01-25T08:17:30+00:00" }, { @@ -1046,6 +1100,10 @@ "spy", "stub" ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" + }, "time": "2020-03-05T15:02:03+00:00" }, { @@ -1110,6 +1168,10 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/1.2" + }, "time": "2017-01-29T09:50:25+00:00" }, { @@ -1162,6 +1224,10 @@ "keywords": [ "diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/1.4" + }, "time": "2017-05-22T07:24:03+00:00" }, { @@ -1229,6 +1295,10 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/master" + }, "time": "2016-11-19T08:54:04+00:00" }, { @@ -1282,6 +1352,10 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" + }, "time": "2016-11-19T07:33:16+00:00" }, { @@ -1347,6 +1421,9 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/phpunit-bridge/tree/v3.4.38" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1366,7 +1443,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "composer/semver": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -1376,5 +1455,5 @@ "platform-overrides": { "php": "5.3.9" }, - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index c90ad2d09..6cf1905f5 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -160,6 +160,7 @@ EOT $installedRepo = new InstalledRepository(array($platformRepo)); if ($composer) { $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); + $installedRepo->addRepository($composer->getRepositoryManager()->getLocalRepository()); } else { $defaultRepos = RepositoryFactory::defaultRepos($io); $repos = new CompositeRepository($defaultRepos); @@ -675,15 +676,17 @@ EOT */ protected function printVersions(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo) { - uasort($versions, 'version_compare'); - $versions = array_keys(array_reverse($versions)); + $versions = array_keys($versions); + $versions = Semver::rsort($versions); // highlight installed version - if ($installedRepo->hasPackage($package)) { - $installedVersion = $package->getPrettyVersion(); - $key = array_search($installedVersion, $versions); - if (false !== $key) { - $versions[$key] = '* ' . $installedVersion . ''; + if ($installedPackages = $installedRepo->findPackages($package->getName())) { + foreach ($installedPackages as $installedPackage) { + $installedVersion = $installedPackage->getPrettyVersion(); + $key = array_search($installedVersion, $versions); + if (false !== $key) { + $versions[$key] = '* ' . $installedVersion . ''; + } } } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 82e649e3f..a03694d31 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -273,6 +273,10 @@ class Problem $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); } foreach ($prepared as $name => $package) { + // remove the implicit dev-master alias to avoid cruft in the display + if (isset($package['versions']['9999999-dev']) && isset($package['versions']['dev-master'])) { + unset($package['versions']['9999999-dev']); + } $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; } diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 22f364beb..a9ddabf7d 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -89,6 +89,11 @@ class ArrayLoader implements LoaderInterface // handle already normalized versions if (isset($config['version_normalized'])) { $version = $config['version_normalized']; + + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained 9999999-dev, we renormalize it + if ($version === '9999999-dev') { + $version = $this->versionParser->normalize($config['version']); + } } else { $version = $this->versionParser->normalize($config['version']); } @@ -320,39 +325,42 @@ class ArrayLoader implements LoaderInterface */ public function getBranchAlias(array $config) { - if (('dev-' !== substr($config['version'], 0, 4) && '-dev' !== substr($config['version'], -4)) - || !isset($config['extra']['branch-alias']) - || !is_array($config['extra']['branch-alias']) - ) { + if ('dev-' !== substr($config['version'], 0, 4) && '-dev' !== substr($config['version'], -4)) { return; } - foreach ($config['extra']['branch-alias'] as $sourceBranch => $targetBranch) { - // ensure it is an alias to a -dev package - if ('-dev' !== substr($targetBranch, -4)) { - continue; - } + if (isset($config['extra']['branch-alias']) && is_array($config['extra']['branch-alias'])) { + foreach ($config['extra']['branch-alias'] as $sourceBranch => $targetBranch) { + // ensure it is an alias to a -dev package + if ('-dev' !== substr($targetBranch, -4)) { + continue; + } - // normalize without -dev and ensure it's a numeric branch that is parseable - $validatedTargetBranch = $this->versionParser->normalizeBranch(substr($targetBranch, 0, -4)); - if ('-dev' !== substr($validatedTargetBranch, -4)) { - continue; - } + // normalize without -dev and ensure it's a numeric branch that is parseable + $validatedTargetBranch = $this->versionParser->normalizeBranch(substr($targetBranch, 0, -4)); + if ('-dev' !== substr($validatedTargetBranch, -4)) { + continue; + } - // ensure that it is the current branch aliasing itself - if (strtolower($config['version']) !== strtolower($sourceBranch)) { - continue; - } + // ensure that it is the current branch aliasing itself + if (strtolower($config['version']) !== strtolower($sourceBranch)) { + continue; + } - // If using numeric aliases ensure the alias is a valid subversion - if (($sourcePrefix = $this->versionParser->parseNumericAliasPrefix($sourceBranch)) - && ($targetPrefix = $this->versionParser->parseNumericAliasPrefix($targetBranch)) - && (stripos($targetPrefix, $sourcePrefix) !== 0) - ) { - continue; - } + // If using numeric aliases ensure the alias is a valid subversion + if (($sourcePrefix = $this->versionParser->parseNumericAliasPrefix($sourceBranch)) + && ($targetPrefix = $this->versionParser->parseNumericAliasPrefix($targetBranch)) + && (stripos($targetPrefix, $sourcePrefix) !== 0) + ) { + continue; + } - return $validatedTargetBranch; + return $validatedTargetBranch; + } + } + + if (in_array($config['version'], array('dev-master', 'dev-default', 'dev-trunk'), true)) { + return '9999999-dev'; } } } diff --git a/src/Composer/Package/Loader/ValidatingArrayLoader.php b/src/Composer/Package/Loader/ValidatingArrayLoader.php index f02f6b165..5efc7ffb5 100644 --- a/src/Composer/Package/Loader/ValidatingArrayLoader.php +++ b/src/Composer/Package/Loader/ValidatingArrayLoader.php @@ -219,7 +219,7 @@ class ValidatingArrayLoader implements LoaderInterface } } - $unboundConstraint = new Constraint('=', $this->versionParser->normalize('dev-master')); + $unboundConstraint = new Constraint('=', '10000000-dev'); $stableConstraint = new Constraint('=', '1.0.0'); foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php index 831c61d5f..3c0a3aff5 100644 --- a/src/Composer/Package/Version/VersionParser.php +++ b/src/Composer/Package/Version/VersionParser.php @@ -70,6 +70,9 @@ class VersionParser extends SemverVersionParser */ public static function isUpgrade($normalizedFrom, $normalizedTo) { + $normalizedFrom = str_replace(array('dev-master', 'dev-trunk', 'dev-default'), '9999999-dev', $normalizedFrom); + $normalizedTo = str_replace(array('dev-master', 'dev-trunk', 'dev-default'), '9999999-dev', $normalizedTo); + if (substr($normalizedFrom, 0, 4) === 'dev-' || substr($normalizedTo, 0, 4) === 'dev-') { return true; } diff --git a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test index d1609ed9a..df25f7478 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test +++ b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test @@ -28,4 +28,5 @@ install --EXPECT-- Installing a/aliased (dev-master abcd) Marking a/aliased (1.0.0) as installed, alias of a/aliased (dev-master abcd) +Marking a/aliased (9999999-dev abcd) as installed, alias of a/aliased (dev-master abcd) Installing b/requirer (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test index 8d928c20d..c13d5fc0e 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test @@ -14,12 +14,12 @@ Aliases take precedence over default package even if default is selected "name": "a/req", "version": "dev-master", "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } }, "source": { "reference": "forked", "type": "git", "url": "" } - } - ] - }, - { - "type": "package", - "package": [ + }, + { + "name": "a/req", "version": "dev-master", + "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } }, + "source": { "reference": "master", "type": "git", "url": "" } + }, { "name": "a/a", "version": "dev-master", "require": { "a/req": "dev-master" } @@ -27,11 +27,6 @@ Aliases take precedence over default package even if default is selected { "name": "a/b", "version": "dev-master", "require": { "a/req": "dev-master" } - }, - { - "name": "a/req", "version": "dev-master", - "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } }, - "source": { "reference": "master", "type": "git", "url": "" } } ] } @@ -66,7 +61,7 @@ Aliases take precedence over default package even if default is selected "aliases": [ { "alias": "dev-master", - "alias_normalized": "9999999-dev", + "alias_normalized": "dev-master", "version": "dev-feature-foo", "package": "a/req" } @@ -88,4 +83,6 @@ install Installing a/req (dev-feature-foo feat.f) Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f) Installing a/a (dev-master) +Marking a/a (9999999-dev) as installed, alias of a/a (dev-master) Installing a/b (dev-master) +Marking a/b (9999999-dev) as installed, alias of a/b (dev-master) diff --git a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test index 63410283d..6fd1f7b53 100644 --- a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test +++ b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test @@ -34,3 +34,4 @@ Installing b/b (dev-foo) Marking b/b (dev-master) as installed, alias of b/b (dev-foo) Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) Installing a/a (dev-master) +Marking a/a (9999999-dev) as installed, alias of a/a (dev-master) diff --git a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test index 5846d13c0..400e932ed 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test @@ -53,3 +53,4 @@ install --prefer-dist } --EXPECT-- Installing a/a (dev-master) +Marking a/a (9999999-dev) as installed, alias of a/a (dev-master) diff --git a/tests/Composer/Test/Fixtures/installer/install-reference.test b/tests/Composer/Test/Fixtures/installer/install-reference.test index f8e696f99..74bf6e40a 100644 --- a/tests/Composer/Test/Fixtures/installer/install-reference.test +++ b/tests/Composer/Test/Fixtures/installer/install-reference.test @@ -21,3 +21,4 @@ Installs a dev package forcing it's reference install --EXPECT-- Installing a/a (dev-master def000) +Marking a/a (9999999-dev def000) as installed, alias of a/a (dev-master def000) diff --git a/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test new file mode 100644 index 000000000..6997e5a77 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test @@ -0,0 +1,31 @@ +--TEST-- +Test that a conflict against >=5 does not include dev-master or other dev-x +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": ">=5", "victim/pkg2": ">=5" } }, + { "name": "victim/pkg", "version": "dev-master" }, + { "name": "victim/pkg2", "version": "dev-foo" } + ] + } + ], + "require": { + "conflicter/pkg": "1.0.0", + "victim/pkg": "*", + "victim/pkg2": "*" + }, + "minimum-stability": "dev" +} + + +--RUN-- +update + +--EXPECT-- +Installing conflicter/pkg (1.0.0) +Installing victim/pkg (dev-master) +Marking victim/pkg (9999999-dev) as installed, alias of victim/pkg (dev-master) +Installing victim/pkg2 (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 8f6715769..4831c7705 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -217,4 +217,5 @@ update a/a b/b d/d g/g Upgrading a/a (dev-master 1111111 => dev-master 2222222) Upgrading b/b (2.0.3 1111111 => 2.0.3 2222222) Installing e/e (dev-master 1111111) +Marking e/e (9999999-dev 1111111) as installed, alias of e/e (dev-master 1111111) Upgrading g/g (dev-master 0000000 => dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test b/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test index 12ad75586..181e039ea 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-to-new-ref-picks-up-changes.test @@ -38,4 +38,5 @@ Updating a dev package to its latest ref should pick up new dependencies update --EXPECT-- Installing a/dependency (dev-master ref) +Marking a/dependency (9999999-dev ref) as installed, alias of a/dependency (dev-master ref) Upgrading a/devpackage (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test index aa86c161e..f0755d0d0 100644 --- a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test +++ b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test @@ -47,3 +47,4 @@ Downgrading from unstable to more stable package should work even if already ins update --EXPECT-- Downgrading a/a (dev-master abcd => 1.0.0) +Marking a/a (9999999-dev abcd) as uninstalled, alias of a/a (dev-master abcd) diff --git a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test index c7e93ccaf..f8bf6010f 100644 --- a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test @@ -45,3 +45,4 @@ update } --EXPECT-- Uninstalling a/a (dev-master 1234) +Marking a/a (9999999-dev 1234) as uninstalled, alias of a/a (dev-master 1234) diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index 5d2fc64c8..68ab1fa1a 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -43,3 +43,4 @@ install --EXPECT-- Uninstalling a/dependency (dev-master ref) Upgrading a/devpackage (dev-master oldref => dev-master newref) +Marking a/dependency (9999999-dev ref) as uninstalled, alias of a/dependency (dev-master ref) diff --git a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php index 25d39270c..93cde83f5 100644 --- a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php @@ -160,6 +160,8 @@ class RootPackageLoaderTest extends TestCase $loader = new RootPackageLoader($manager, $config, null, new VersionGuesser($config, $executor, new VersionParser())); $package = $loader->load(array('require' => array('foo/bar' => 'self.version'))); + $this->assertEquals("9999999-dev", $package->getPrettyVersion()); + $package = $package->getAliasOf(); $this->assertEquals("dev-master", $package->getPrettyVersion()); } diff --git a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php index 2cde001ac..1b7565992 100644 --- a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php @@ -356,7 +356,6 @@ class ValidatingArrayLoaderTest extends TestCase 'require' => array( 'foo/baz' => '*', 'bar/baz' => '>=1.0', - 'bar/foo' => 'dev-master', 'bar/hacked' => '@stable', 'bar/woo' => '1.0.0', ), @@ -364,7 +363,6 @@ class ValidatingArrayLoaderTest extends TestCase array( 'require.foo/baz : unbound version constraints (*) should be avoided', 'require.bar/baz : unbound version constraints (>=1.0) should be avoided', - 'require.bar/foo : unbound version constraints (dev-master) should be avoided', 'require.bar/hacked : unbound version constraints (@stable) should be avoided', 'require.bar/woo : exact version constraints (1.0.0) should be avoided if the package follows semantic versioning', ), diff --git a/tests/Composer/Test/Package/Version/VersionGuesserTest.php b/tests/Composer/Test/Package/Version/VersionGuesserTest.php index 31c47d72b..9527b628f 100644 --- a/tests/Composer/Test/Package/Version/VersionGuesserTest.php +++ b/tests/Composer/Test/Package/Version/VersionGuesserTest.php @@ -89,7 +89,7 @@ class VersionGuesserTest extends TestCase $guesser = new VersionGuesser($config, $executor, new VersionParser()); $versionArray = $guesser->guessVersion(array(), 'dummy/path'); - $this->assertEquals("9999999-dev", $versionArray['version']); + $this->assertEquals("dev-".$branch, $versionArray['version']); $this->assertEquals("dev-".$branch, $versionArray['pretty_version']); $this->assertEmpty($versionArray['commit']); } @@ -124,7 +124,7 @@ class VersionGuesserTest extends TestCase $guesser = new VersionGuesser($config, $executor, new VersionParser()); $versionArray = $guesser->guessVersion(array(), 'dummy/path'); - $this->assertEquals("9999999-dev", $versionArray['version']); + $this->assertEquals("dev-master", $versionArray['version']); $this->assertEquals("dev-master", $versionArray['pretty_version']); $this->assertArrayNotHasKey('feature_version', $versionArray); $this->assertArrayNotHasKey('feature_pretty_version', $versionArray); From d13ce20b6ec88453ff87cde7daf48c845827c41b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 11 Mar 2020 16:11:12 +0100 Subject: [PATCH 266/321] Fix handling of composer repos with v1 version_normalized format --- src/Composer/Repository/ComposerRepository.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index a0efb07a2..a2c72d681 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -560,6 +560,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if (!isset($versionsToLoad[$version['uid']])) { if (!isset($version['version_normalized'])) { $version['version_normalized'] = $this->versionParser->normalize($version['version']); + } elseif ($version['version_normalized'] === '9999999-dev') { + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained 9999999-dev, we renormalize it + $version['version_normalized'] = $this->versionParser->normalize($version['version']); } if ($this->isVersionAcceptable($acceptableStabilities, $stabilityFlags, null, $normalizedName, $version)) { @@ -678,6 +681,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito foreach ($versions as $version) { if (!isset($version['version_normalized'])) { $version['version_normalized'] = $repo->versionParser->normalize($version['version']); + } elseif ($version['version_normalized'] === '9999999-dev') { + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained 9999999-dev, we renormalize it + $version['version_normalized'] = $this->versionParser->normalize($version['version']); } if ($repo->isVersionAcceptable($acceptableStabilities, $stabilityFlags, $constraint, $realName, $version)) { From ba04a46caed9459bd87e6079ec30af3060852303 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 11 Mar 2020 16:20:35 +0100 Subject: [PATCH 267/321] Fix 5.3 issue --- src/Composer/Repository/ComposerRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index a2c72d681..2c153528a 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -683,7 +683,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $version['version_normalized'] = $repo->versionParser->normalize($version['version']); } elseif ($version['version_normalized'] === '9999999-dev') { // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained 9999999-dev, we renormalize it - $version['version_normalized'] = $this->versionParser->normalize($version['version']); + $version['version_normalized'] = $repo->versionParser->normalize($version['version']); } if ($repo->isVersionAcceptable($acceptableStabilities, $stabilityFlags, $constraint, $realName, $version)) { From 589aa351a829b7ff03bff84f5c1678f4436e0907 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 11 Mar 2020 16:44:08 +0100 Subject: [PATCH 268/321] Change Uninstalling to Removing in lock operations --- .../DependencyResolver/Operation/UninstallOperation.php | 2 +- .../Fixtures/installer/update-removes-unused-locked-dep.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index 704635b2a..b825796ed 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -61,7 +61,7 @@ class UninstallOperation extends SolverOperation */ public function show($lock) { - return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; + return ($lock ? 'Removing ' : 'Uninstalling ').$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; } /** diff --git a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test index 808afb02e..2b377506d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test +++ b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test @@ -56,7 +56,7 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 0 updates, 1 removal - - Uninstalling b/b (1.0.0) + - Removing b/b (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 0 updates, 2 removals From 9c84f4d79fe778ec8f5874a2468ff72fb3d3dda1 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 11 Mar 2020 17:02:25 +0100 Subject: [PATCH 269/321] Make sure InstalledRepository itself can be added too but requires allowing installed repos in reposet --- src/Composer/Repository/RepositorySet.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 98bc36bc6..5c7ac1bb8 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -22,6 +22,7 @@ use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\LockArrayRepository; use Composer\Repository\InstalledRepositoryInterface; +use Composer\Repository\InstalledRepository; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Package\Version\StabilityFilter; @@ -190,7 +191,7 @@ class RepositorySet $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher); foreach ($this->repositories as $repo) { - if ($repo instanceof InstalledRepositoryInterface && !$this->allowInstalledRepositories) { + if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { throw new \LogicException('The pool can not accept packages from an installed repository'); } } From ddb1e79bef66a8145d91344c73deaa9498dcb4b2 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 11 Mar 2020 16:38:53 +0100 Subject: [PATCH 270/321] Handle dev extraction exit codes instead of completing broken lock with errors --- src/Composer/Installer.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 880840785..9e9d32407 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -429,7 +429,10 @@ class Installer $this->io->writeError('Nothing to modify in lock file'); } - $this->extractDevPackages($lockTransaction, $platformRepo, $aliases, $policy); + $exitCode = $this->extractDevPackages($lockTransaction, $platformRepo, $aliases, $policy); + if ($exitCode !== 0) { + return $exitCode; + } // write lock $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); @@ -542,7 +545,7 @@ class Installer protected function extractDevPackages(LockTransaction $lockTransaction, $platformRepo, $aliases, $policy) { if (!$this->package->getDevRequires()) { - return array(); + return 0; } $resultRepo = new ArrayRepository(array()); @@ -570,12 +573,15 @@ class Installer $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); + $this->io->writeError('Your requirements can be successfully resolved when require-dev packages are present.'); $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); return max(1, $e->getCode()); } $lockTransaction->setNonDevPackages($nonDevLockTransaction); + + return 0; } /** From 1f467046d78c2647c96a96073c4d9c60b107134d Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 11 Mar 2020 17:38:16 +0100 Subject: [PATCH 271/321] Implement getProviders on reposet for all repo types and add replacers This way errors during require dev extraction make more sense --- src/Composer/DependencyResolver/Problem.php | 2 +- .../SolverProblemsException.php | 4 +- src/Composer/Installer.php | 5 ++- src/Composer/Repository/RepositorySet.php | 22 ++++++++-- ...er-dev-require-cannot-satisfy-require.test | 40 +++++++++++++++++++ 5 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/provider-dev-require-cannot-satisfy-require.test diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index a03694d31..a68d45b40 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -248,7 +248,7 @@ class Problem return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'); } - if ($providers = $repositorySet->getProviders($packageName)) { + if ($providers = $repositorySet->getProvidersAndReplacers($packageName)) { $maxProviders = 20; $providersStr = implode(array_map(function ($p) { return " - ${p['name']} ".substr($p['description'], 0, 100)."\n"; diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 542fe0464..a47bd1c8c 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -31,7 +31,7 @@ class SolverProblemsException extends \RuntimeException parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2); } - public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isDevExtraction = false) { $installedMap = $request->getPresentMap(true); $text = "\n"; @@ -44,7 +44,7 @@ class SolverProblemsException extends \RuntimeException } } - if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) { + if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) { $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead for further common problems."; } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9e9d32407..7ed0ccda6 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -573,8 +573,9 @@ class Installer $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); - $this->io->writeError('Your requirements can be successfully resolved when require-dev packages are present.'); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); + $this->io->writeError('Your requirements can be resolved successfully when require-dev packages are present.'); + $this->io->writeError('You may need to move packages from require-dev or some of their dependencies to require.'); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, true)); return max(1, $e->getCode()); } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 5c7ac1bb8..506596a1b 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -163,17 +163,31 @@ class RepositorySet return $candidates; } - public function getProviders($packageName) + public function getProvidersAndReplacers($packageName) { + $providers = array(); foreach ($this->repositories as $repository) { if ($repository instanceof ComposerRepository) { - if ($providers = $repository->getProviders($packageName)) { - return $providers; + if ($repoProviders = $repository->getProviders($packageName)) { + $providers = array_merge($providers, $repoProviders); + } + } else { + foreach ($repository->getPackages() as $candidate) { + foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { + if ($packageName === $link->getTarget()) { + $providers[] = array( + 'name' => $candidate->getName(), + 'description' => $candidate->getDescription(), + 'type' => $candidate->getType(), + ); + continue 2; + } + } } } } - return array(); + return $providers; } public function isPackageAcceptable($names, $stability) diff --git a/tests/Composer/Test/Fixtures/installer/provider-dev-require-cannot-satisfy-require.test b/tests/Composer/Test/Fixtures/installer/provider-dev-require-cannot-satisfy-require.test new file mode 100644 index 000000000..05d03e69c --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-dev-require-cannot-satisfy-require.test @@ -0,0 +1,40 @@ +--TEST-- +Test that an appropriate error is thrown if a requirement is only satisfied by a package provided by a dependency of a dev requirement. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + {"name": "provider/requirer", "version": "1.0.0", "type": "metapackage", "require": {"b/b": "1.0.0"}}, + {"name": "b/b", "version": "1.0.0", "type": "metapackage", "provide": {"provided/pkg": "1.0.0"}} + ] + } + ], + "require": { + "provided/pkg": "1.0.0" + }, + "require-dev": { + "provider/requirer": "1.0.0" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Unable to find a compatible set of packages based on your non-dev requirements alone. +Your requirements can be resolved successfully when require-dev packages are present. +You may need to move packages from require-dev or some of their dependencies to require. + + Problem 1 + - Root composer.json requires provided/pkg 1.0.0, it could not be found in any version, but the following packages provide it: + - b/b + Consider requiring one of these to satisfy the provided/pkg requirement. + +--EXPECT-- From 06f460c557172ce59699570c38eb6d53cc2d41cc Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 11 Mar 2020 21:32:55 +0100 Subject: [PATCH 272/321] Remove trailing spaces from output --- src/Composer/DependencyResolver/Problem.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index a68d45b40..5f22914cc 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -251,12 +251,13 @@ class Problem if ($providers = $repositorySet->getProvidersAndReplacers($packageName)) { $maxProviders = 20; $providersStr = implode(array_map(function ($p) { - return " - ${p['name']} ".substr($p['description'], 0, 100)."\n"; + $description = $p['description'] ? ' '.substr($p['description'], 0, 100) : ''; + return " - ${p['name']}".$description."\n"; }, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers)); if (count($providers) > $maxProviders+1) { $providersStr .= ' ... and '.(count($providers)-$maxProviders).' more.'."\n"; } - return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it: \n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement."); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it:\n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement."); } return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name."); From 73a5cc63cc631fe66fa1f532cf5965c2879aff3a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 Mar 2020 08:12:12 +0100 Subject: [PATCH 273/321] Always use Removing for consistency between dry-run and actual install --- .../DependencyResolver/Operation/UninstallOperation.php | 2 +- .../Fixtures/installer/install-from-lock-removes-package.test | 2 +- .../Fixtures/installer/update-no-dev-still-resolves-dev.test | 2 +- .../Fixtures/installer/update-removes-unused-locked-dep.test | 4 ++-- .../Test/Fixtures/installer/update-to-empty-from-locked.test | 2 +- .../Fixtures/installer/update-whitelist-removes-unused.test | 2 +- .../installer/updating-dev-from-lock-removes-old-deps.test | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index b825796ed..2ddc2f169 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -61,7 +61,7 @@ class UninstallOperation extends SolverOperation */ public function show($lock) { - return ($lock ? 'Removing ' : 'Uninstalling ').$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; + return 'Removing '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; } /** diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index b1677956c..b996ff65b 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -40,5 +40,5 @@ Install from a lock file that deleted a package --RUN-- install --EXPECT-- -Uninstalling old/dependency (1.0.0) +Removing old/dependency (1.0.0) Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test index f0c8282a1..06cbc27c7 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test @@ -60,7 +60,7 @@ Updates with --no-dev but we still end up with a complete lock file including de --RUN-- update --no-dev --EXPECT-- -Uninstalling a/b (1.0.0) +Removing a/b (1.0.0) Upgrading a/a (1.0.0 => 1.0.1) Installing a/c (1.0.0) Upgrading dev/pkg (dev-master old => dev-master new) diff --git a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test index 2b377506d..a47afb7ff 100644 --- a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test +++ b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test @@ -63,5 +63,5 @@ Package operations: 0 installs, 0 updates, 2 removals Generating autoload files --EXPECT-- -Uninstalling c/c (1.0.0) -Uninstalling b/b (1.0.0) +Removing c/c (1.0.0) +Removing b/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test index f8bf6010f..89e94d781 100644 --- a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test @@ -44,5 +44,5 @@ update "platform-dev": [] } --EXPECT-- -Uninstalling a/a (dev-master 1234) +Removing a/a (dev-master 1234) Marking a/a (9999999-dev 1234) as uninstalled, alias of a/a (dev-master 1234) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test index 0863d1321..9360bc2f6 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -44,5 +44,5 @@ Update with a package whitelist removes unused packages --RUN-- update --with-dependencies whitelisted/pkg --EXPECT-- -Uninstalling old/dependency (1.0.0) +Removing old/dependency (1.0.0) Upgrading whitelisted/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index 68ab1fa1a..3fb6654ab 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -41,6 +41,6 @@ Installing locked dev packages should remove old dependencies --RUN-- install --EXPECT-- -Uninstalling a/dependency (dev-master ref) +Removing a/dependency (dev-master ref) Upgrading a/devpackage (dev-master oldref => dev-master newref) Marking a/dependency (9999999-dev ref) as uninstalled, alias of a/dependency (dev-master ref) From 3d0d71367d596019015e9278a58eb9ad8f029982 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 Mar 2020 08:37:01 +0100 Subject: [PATCH 274/321] Doc updates --- src/Composer/Plugin/PluginInterface.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index 27b8c9754..4390764ff 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -40,6 +40,10 @@ interface PluginInterface /** * Remove any hooks from Composer * + * This will be called when a plugin is deactivated before being + * uninstalled, but also before it gets upgraded to a new version + * so the old one can be deactivated and the new one activated. + * * @param Composer $composer * @param IOInterface $io */ @@ -48,7 +52,7 @@ interface PluginInterface /** * Prepare the plugin to be uninstalled * - * This will be called after deactivate + * This will be called after deactivate. * * @param Composer $composer * @param IOInterface $io From 281d8930ff1c15bd2a5b0dccea3ac8e87f6bd533 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 12 Mar 2020 12:19:46 +0100 Subject: [PATCH 275/321] For dev extraction skip pool building, we already have a working package set Also reduce getProviders back to just providers, and add some todos --- src/Composer/DependencyResolver/Problem.php | 2 +- src/Composer/Installer.php | 2 +- src/Composer/Repository/ComposerRepository.php | 2 ++ src/Composer/Repository/RepositorySet.php | 18 +++++++++++++----- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 5f22914cc..ad9989d19 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -248,7 +248,7 @@ class Problem return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'); } - if ($providers = $repositorySet->getProvidersAndReplacers($packageName)) { + if ($providers = $repositorySet->getProviders($packageName)) { $maxProviders = 20; $providersStr = implode(array_map(function ($p) { $description = $p['description'] ? ' '.substr($p['description'], 0, 100) : ''; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 7ed0ccda6..2bb38f885 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -565,7 +565,7 @@ class Installer $request->requireName($link->getTarget(), $link->getConstraint()); } - $pool = $repositorySet->createPool($request, $this->eventDispatcher); + $pool = $repositorySet->createPool($request, $this->eventDispatcher, true); $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 2c153528a..e982de5b2 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -422,11 +422,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito public function getProviders($packageName) { if (!$this->providersApiUrl) { + // TODO should this return the info based on getPackages in other cases? return array(); } $result = $this->httpDownloader->get(str_replace('%package%', $packageName, $this->providersApiUrl), $this->options)->decodeJson(); + // TODO filter packageName out here? return $result['providers']; } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 506596a1b..b0d4153cd 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -163,7 +163,7 @@ class RepositorySet return $candidates; } - public function getProvidersAndReplacers($packageName) + public function getProviders($packageName) { $providers = array(); foreach ($this->repositories as $repository) { @@ -173,7 +173,7 @@ class RepositorySet } } else { foreach ($repository->getPackages() as $candidate) { - foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { + foreach ($candidate->getProvides() as $link) { if ($packageName === $link->getTarget()) { $providers[] = array( 'name' => $candidate->getName(), @@ -200,10 +200,8 @@ class RepositorySet * * @return Pool */ - public function createPool(Request $request, EventDispatcher $eventDispatcher = null) + public function createPool(Request $request, EventDispatcher $eventDispatcher = null, $allPackages = false) { - $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher); - foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { throw new \LogicException('The pool can not accept packages from an installed repository'); @@ -212,6 +210,16 @@ class RepositorySet $this->locked = true; + if ($allPackages) { + $packages = array(); + foreach ($this->repositories as $repository) { + $packages = array_merge($packages, $repository->getPackages()); + } + return new Pool($packages); + } + + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher); + return $poolBuilder->buildPool($this->repositories, $request); } From 3ec59204fc021ff0d07bccb4f91ba3d9bd4d03f9 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 12 Mar 2020 12:28:46 +0100 Subject: [PATCH 276/321] Update test: require-dev is now allowed to satisfy providers from require --- ...ider-dev-require-can-satisfy-require.test} | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) rename tests/Composer/Test/Fixtures/installer/{provider-dev-require-cannot-satisfy-require.test => provider-dev-require-can-satisfy-require.test} (51%) diff --git a/tests/Composer/Test/Fixtures/installer/provider-dev-require-cannot-satisfy-require.test b/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test similarity index 51% rename from tests/Composer/Test/Fixtures/installer/provider-dev-require-cannot-satisfy-require.test rename to tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test index 05d03e69c..8d29112f0 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-dev-require-cannot-satisfy-require.test +++ b/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test @@ -20,21 +20,33 @@ Test that an appropriate error is thrown if a requirement is only satisfied by a } --RUN-- -update - ---EXPECT-EXIT-CODE-- -2 - ---EXPECT-OUTPUT-- -Loading composer repositories with package information -Updating dependencies -Unable to find a compatible set of packages based on your non-dev requirements alone. -Your requirements can be resolved successfully when require-dev packages are present. -You may need to move packages from require-dev or some of their dependencies to require. - - Problem 1 - - Root composer.json requires provided/pkg 1.0.0, it could not be found in any version, but the following packages provide it: - - b/b - Consider requiring one of these to satisfy the provided/pkg requirement. +update --no-dev +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "b/b", + "version": "1.0.0", + "type": "metapackage", + "provide": {"provided/pkg": "1.0.0"} + } + ], + "packages-dev": [ + { + "name": "provider/requirer", + "version": "1.0.0", + "type": "metapackage", + "require": {"b/b": "1.0.0"} + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --EXPECT-- +Installing b/b (1.0.0) From ee8df484c42ab6df0f4434864c98d141d9792757 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 12 Mar 2020 13:17:04 +0100 Subject: [PATCH 277/321] Separate createPool and createPoolWithAllPackages, fix test description --- src/Composer/Installer.php | 2 +- src/Composer/Repository/RepositorySet.php | 36 +++++++++++++------ ...vider-dev-require-can-satisfy-require.test | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 2bb38f885..ba06f44e9 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -565,7 +565,7 @@ class Installer $request->requireName($link->getTarget(), $link->getConstraint()); } - $pool = $repositorySet->createPool($request, $this->eventDispatcher, true); + $pool = $repositorySet->createPoolWithAllPackages(); $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index b0d4153cd..a05906d68 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -200,7 +200,27 @@ class RepositorySet * * @return Pool */ - public function createPool(Request $request, EventDispatcher $eventDispatcher = null, $allPackages = false) + public function createPool(Request $request, EventDispatcher $eventDispatcher = null) + { + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher); + + foreach ($this->repositories as $repo) { + if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { + throw new \LogicException('The pool can not accept packages from an installed repository'); + } + } + + $this->locked = true; + + return $poolBuilder->buildPool($this->repositories, $request); + } + + /** + * Create a pool for dependency resolution from the packages in this repository set. + * + * @return Pool + */ + public function createPoolWithAllPackages() { foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { @@ -210,17 +230,11 @@ class RepositorySet $this->locked = true; - if ($allPackages) { - $packages = array(); - foreach ($this->repositories as $repository) { - $packages = array_merge($packages, $repository->getPackages()); - } - return new Pool($packages); + $packages = array(); + foreach ($this->repositories as $repository) { + $packages = array_merge($packages, $repository->getPackages()); } - - $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher); - - return $poolBuilder->buildPool($this->repositories, $request); + return new Pool($packages); } // TODO unify this with above in some simpler version without "request"? diff --git a/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test b/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test index 8d29112f0..b5d3fefe7 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test +++ b/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test @@ -1,5 +1,5 @@ --TEST-- -Test that an appropriate error is thrown if a requirement is only satisfied by a package provided by a dependency of a dev requirement. +Test that a requirement can be satisfied by a providing package required in require-dev. --COMPOSER-- { "repositories": [ From 8a83d5cc353ea3f561226a01accb92bbe2539cc0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 Mar 2020 13:50:18 +0100 Subject: [PATCH 278/321] Fix create-project command --- src/Composer/Command/CreateProjectCommand.php | 2 -- src/Composer/Downloader/DownloadManager.php | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 52c75a609..9263ba7ee 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -183,8 +183,6 @@ EOT $composer = Factory::create($io, null, $disablePlugins); } - $composer->getDownloadManager()->setOutputProgress(!$noProgress); - $fs = new Filesystem(); if ($noScripts === false) { diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index 794998bb2..3ee786c17 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -175,6 +175,7 @@ class DownloadManager */ public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) { + $targetDir = $this->normalizeTargetDir($targetDir); $this->filesystem->ensureDirectoryExists(dirname($targetDir)); $sources = $this->getAvailableSources($package, $prevPackage); @@ -244,6 +245,7 @@ class DownloadManager */ public function prepare($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) { + $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($package); if ($downloader) { return $downloader->prepare($type, $package, $targetDir, $prevPackage); @@ -262,6 +264,7 @@ class DownloadManager */ public function install(PackageInterface $package, $targetDir) { + $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($package); if ($downloader) { return $downloader->install($package, $targetDir); @@ -280,6 +283,7 @@ class DownloadManager */ public function update(PackageInterface $initial, PackageInterface $target, $targetDir) { + $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($target); $initialDownloader = $this->getDownloaderForPackage($initial); @@ -332,6 +336,7 @@ class DownloadManager */ public function remove(PackageInterface $package, $targetDir) { + $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($package); if ($downloader) { return $downloader->remove($package, $targetDir); @@ -350,6 +355,7 @@ class DownloadManager */ public function cleanup($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) { + $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($package); if ($downloader) { return $downloader->cleanup($type, $package, $targetDir, $prevPackage); @@ -422,4 +428,20 @@ class DownloadManager return $sources; } + + /** + * Downloaders expect a /path/to/dir without trailing slash + * + * If any Installer provides a path with a trailing slash, this can cause bugs so make sure we remove them + * + * @return string + */ + private function normalizeTargetDir($dir) + { + if ($dir === '\\' || $dir === '/') { + return $dir; + } + + return rtrim($dir, '\\/'); + } } From 08cee4c3e986717a42f00d8ab91e17a7eb90fd55 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 Mar 2020 15:30:20 +0100 Subject: [PATCH 279/321] Implement getProviders equally on all repos --- src/Composer/Repository/ArrayRepository.php | 27 ++++++++++++++ .../Repository/ComposerRepository.php | 37 ++++++++++++++++--- .../Repository/CompositeRepository.php | 14 +++++++ .../Repository/LockArrayRepository.php | 2 +- .../Repository/RepositoryInterface.php | 11 ++++++ src/Composer/Repository/RepositorySet.php | 19 +--------- .../Fixtures/installer/solver-problems.test | 36 ++++++++++-------- 7 files changed, 107 insertions(+), 39 deletions(-) diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index f63f80753..69ea0d080 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -204,6 +204,33 @@ class ArrayRepository implements RepositoryInterface $this->packageMap = null; } + /** + * {@inheritDoc} + */ + public function getProviders($packageName) + { + $result = array(); + + foreach ($this->getPackages() as $candidate) { + if (isset($result[$candidate->getName()])) { + continue; + } + foreach ($candidate->getProvides() as $link) { + if ($packageName === $link->getTarget()) { + $result[$candidate->getName()] = array( + 'name' => $candidate->getName(), + 'description' => $candidate->getDescription(), + 'type' => $candidate->getType(), + 'repository' => $candidate->getSourceUrl() ?: '', + ); + continue 2; + } + } + } + + return $result; + } + protected function createAliasPackage(PackageInterface $package, $alias, $prettyAlias) { return new AliasPackage($package instanceof AliasPackage ? $package->getAliasOf() : $package, $alias, $prettyAlias); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e982de5b2..dc455dc8d 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -421,15 +421,40 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito public function getProviders($packageName) { - if (!$this->providersApiUrl) { - // TODO should this return the info based on getPackages in other cases? - return array(); + $this->loadRootServerFile(); + $result = array(); + + if ($this->providersApiUrl) { + $apiResult = $this->httpDownloader->get(str_replace('%package%', $packageName, $this->providersApiUrl), $this->options)->decodeJson(); + + foreach ($apiResult['providers'] as $provider) { + $result[$provider['name']] = $provider; + } + + return $result; } - $result = $this->httpDownloader->get(str_replace('%package%', $packageName, $this->providersApiUrl), $this->options)->decodeJson(); + if ($this->hasPartialPackages()) { + foreach ($this->partialPackagesByName as $versions) { + foreach ($versions as $candidate) { + if (isset($result[$candidate['name']]) || !isset($candidate['provide'][$packageName])) { + continue; + } + $result[$candidate['name']] = array( + 'name' => $candidate['name'], + 'description' => isset($candidate['description']) ? $candidate['description'] : '', + 'type' => isset($candidate['type']) ? $candidate['type'] : '', + 'repository' => '', + ); + } + } + } - // TODO filter packageName out here? - return $result['providers']; + if ($this->packages) { + $result = array_merge($result, parent::getProviders($packageName)); + } + + return $result; } private function getProviderNames() diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index 5242da688..acabaffb0 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -147,6 +147,20 @@ class CompositeRepository implements RepositoryInterface return $packages ? call_user_func_array('array_merge', $packages) : array(); } + /** + * {@inheritdoc} + */ + public function getProviders($packageName) + { + $results = array(); + foreach ($this->repositories as $repository) { + /* @var $repository RepositoryInterface */ + $results[] = $repository->getProviders($packageName); + } + + return $results ? call_user_func_array('array_merge', $results) : array(); + } + /** * {@inheritdoc} */ diff --git a/src/Composer/Repository/LockArrayRepository.php b/src/Composer/Repository/LockArrayRepository.php index 8da3d5915..30bbaa5b2 100644 --- a/src/Composer/Repository/LockArrayRepository.php +++ b/src/Composer/Repository/LockArrayRepository.php @@ -19,7 +19,7 @@ namespace Composer\Repository; * * @author Nils Adermann */ -class LockArrayRepository extends ArrayRepository implements RepositoryInterface +class LockArrayRepository extends ArrayRepository { public function getRepoName() { diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 9992778fb..75813c085 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -84,6 +84,17 @@ interface RepositoryInterface extends \Countable */ public function search($query, $mode = 0, $type = null); + /** + * Returns a list of packages providing a given package name + * + * Packages which have the same name as $packageName should not be returned, only those that have a "provide" on it. + * + * @param string $packageName package name which must be provided + * + * @return array[] an array with the provider name as key and value of array('name' => '...', 'description' => '...', 'type' => '...', 'repository' => '...url to source repo if available...') + */ + public function getProviders($packageName); + /** * Returns a name representing this repository to the user * diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index a05906d68..a2efbdc67 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -167,23 +167,8 @@ class RepositorySet { $providers = array(); foreach ($this->repositories as $repository) { - if ($repository instanceof ComposerRepository) { - if ($repoProviders = $repository->getProviders($packageName)) { - $providers = array_merge($providers, $repoProviders); - } - } else { - foreach ($repository->getPackages() as $candidate) { - foreach ($candidate->getProvides() as $link) { - if ($packageName === $link->getTarget()) { - $providers[] = array( - 'name' => $candidate->getName(), - 'description' => $candidate->getDescription(), - 'type' => $candidate->getType(), - ); - continue 2; - } - } - } + if ($repoProviders = $repository->getProviders($packageName)) { + $providers = array_merge($providers, $repoProviders); } } diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index f3905b6fa..005047127 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -47,7 +47,8 @@ Test the error output of solver problems. { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "dependency/unstable-pkg", "version": "1.0.0-dev" }, { "name": "stable-requiree-excluded/pkg", "version": "1.0.1" }, - { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }, + { "name": "api/provider", "description": "Provides the missing API", "version": "1.0.0", "provide": { "missing/provided-api": "1.*" } } ] } ], @@ -59,6 +60,7 @@ Test the error output of solver problems. "package/found5": "2.*", "package/found6": "2.*", "package/found7": "2.*", + "missing/provided-api": "2.*", "conflict/requirer": "2.*", "conflict/requirer2": "2.*", "unstable/package": "2.*", @@ -107,41 +109,45 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Root composer.json requires unstable/package 2.*, found unstable/package[2.0.0-alpha] but it does not match your minimum-stability. + - Root composer.json requires missing/provided-api 2.*, it could not be found in any version, but the following packages provide it: + - api/provider Provides the missing API + Consider requiring one of these to satisfy the missing/provided-api requirement. Problem 2 - - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. + - Root composer.json requires unstable/package 2.*, found unstable/package[2.0.0-alpha] but it does not match your minimum-stability. Problem 3 - - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update. + - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. Problem 4 - - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. + - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update. Problem 5 - - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension. + - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. Problem 6 - - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version (%s) installed. Install or enable PHP's xml extension. + - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension. Problem 7 - - Root composer.json requires php 1 but your php version (%s) does not satisfy that requirement. + - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version (%s) installed. Install or enable PHP's xml extension. Problem 8 + - Root composer.json requires php 1 but your php version (%s) does not satisfy that requirement. + Problem 9 - Root composer.json requires package/found 2.* -> satisfiable by package/found[2.0.0]. - package/found 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. - Problem 9 + Problem 10 - Root composer.json requires package/found2 2.* -> satisfiable by package/found2[2.0.0]. - package/found2 2.0.0 requires invalid/💩package * -> could not be found, it looks like its name is invalid, "💩" is not allowed in package names. - Problem 10 + Problem 11 - Root composer.json requires package/found3 2.* -> satisfiable by package/found3[2.0.0]. - package/found3 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. - Problem 11 + Problem 12 - Root composer.json requires package/found4 2.* -> satisfiable by package/found4[2.0.0]. - package/found4 2.0.0 requires non-existent/pkg2 1.* -> could not be found in any version, there may be a typo in the package name. - Problem 12 + Problem 13 - Root composer.json requires package/found6 2.* -> satisfiable by package/found6[2.0.0]. - package/found6 2.0.0 requires stable-requiree-excluded/pkg2 1.0.1 -> found stable-requiree-excluded/pkg2[1.0.0] but it does not match your constraint. - Problem 13 + Problem 14 - Root composer.json requires package/found7 2.* -> satisfiable by package/found7[2.0.0]. - package/found7 2.0.0 requires php-64bit 1.0.1 -> your php-64bit version (%s) does not satisfy that requirement. - Problem 14 + Problem 15 - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). - Problem 15 + Problem 16 - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). - package/found5 2.0.0 requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - Root composer.json requires package/found5 2.* -> satisfiable by package/found5[2.0.0]. From 82e2a679bfedcab2b4932420dc52367724998467 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 17:30:36 +0100 Subject: [PATCH 280/321] Add TODO note --- src/Composer/DependencyResolver/PoolBuilder.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index d3d14e629..1a41dcd98 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -72,6 +72,9 @@ class PoolBuilder } } + // TODO make sure if a fixed package replaces X packages they are not loaded again in loadNames + // perhaps loadPackage needs to mark them as loadedNames when loading a fixed package? + foreach ($request->getRequires() as $packageName => $constraint) { if (isset($this->loadedNames[$packageName])) { continue; From d58653627a70984f9d38787253bc23dcb6704f1d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 17 Jan 2020 17:39:36 +0100 Subject: [PATCH 281/321] Optimize loading of deps from fixed packages --- .../DependencyResolver/PoolBuilder.php | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 1a41dcd98..1abaa4113 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -76,15 +76,29 @@ class PoolBuilder // perhaps loadPackage needs to mark them as loadedNames when loading a fixed package? foreach ($request->getRequires() as $packageName => $constraint) { + // fixed packages do not need to get filtered as they are pinned already if (isset($this->loadedNames[$packageName])) { continue; } - // TODO currently lock above is always NULL if we adjust that, this needs to merge constraints - // TODO does it really make sense that we can have install requests for the same package that is actively locked with non-matching constraints? - // also see the solver-problems.test test case - $constraint = array_key_exists($packageName, $loadNames) ? null : $constraint; - $loadNames[$packageName] = $constraint; - $this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null; + + $loadNames[$packageName] = null; + if ($constraint) { + if (!array_key_exists($packageName, $this->nameConstraints)) { + $this->nameConstraints[$packageName] = new MultiConstraint(array($constraint), false); + } elseif ($this->nameConstraints[$packageName]) { + // TODO addConstraint function? + $this->nameConstraints[$packageName] = new MultiConstraint(array_merge(array($constraint), $this->nameConstraints[$packageName]->getConstraints()), false); + } + } + } + + // all the merged constraints from install requests + fixed packages can be applied + // when loading package metadata already, as these are set in stone + foreach ($this->nameConstraints as $package => $constraint) { + if ($constraint !== null && array_key_exists($package, $loadNames)) { + $loadNames[$package] = $constraint; + unset($this->nameConstraints[$package]); + } } while (!empty($loadNames)) { @@ -210,6 +224,8 @@ class PoolBuilder $loadNames[$require] = null; } if ($linkConstraint = $link->getConstraint()) { + // TODO check if linkConstraint is EmptyConstraint then set to null as well? + if (!array_key_exists($require, $this->nameConstraints)) { $this->nameConstraints[$require] = new MultiConstraint(array($linkConstraint), false); } elseif ($this->nameConstraints[$require]) { From 59bc957e760b21e0987dfb87ecb6d995f6596c62 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 29 Jan 2020 17:57:45 +0100 Subject: [PATCH 282/321] Simplify loading of fixed and root require packages in pool builder additionally mark all packages replaced by fixed packages as loaded as there is no need to load those names at all, since the fixed package will provide them --- .../DependencyResolver/PoolBuilder.php | 38 ++++++++++--------- .../Fixtures/installer/solver-problems.test | 2 + 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 1abaa4113..dad0da6ba 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -53,14 +53,21 @@ class PoolBuilder public function buildPool(array $repositories, Request $request) { - $pool = new Pool(); - - // TODO do we really want the request here? kind of want a root requirements thingy instead $loadNames = array(); foreach ($request->getFixedPackages() as $package) { + // TODO do we need to add this to nameConstraints at all? $this->nameConstraints[$package->getName()] = null; $this->loadedNames[$package->getName()] = true; - unset($loadNames[$package->getName()]); + + // replace means conflict, so if a fixed package replaces a name, no need to load that one, packages would conflict anyways + foreach ($package->getReplaces() as $link) { + $this->nameConstraints[$package->getName()] = null; + $this->loadedNames[$link->getTarget()] = true; + } + + // TODO in how far can we do the above for conflicts? It's more tricky cause conflicts can be limited to + // specific versions while replace is a conflict with all versions of the name + if ( $package->getRepository() instanceof RootPackageRepository || $package->getRepository() instanceof PlatformRepository @@ -72,24 +79,14 @@ class PoolBuilder } } - // TODO make sure if a fixed package replaces X packages they are not loaded again in loadNames - // perhaps loadPackage needs to mark them as loadedNames when loading a fixed package? - foreach ($request->getRequires() as $packageName => $constraint) { - // fixed packages do not need to get filtered as they are pinned already + // 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])) { continue; } - $loadNames[$packageName] = null; - if ($constraint) { - if (!array_key_exists($packageName, $this->nameConstraints)) { - $this->nameConstraints[$packageName] = new MultiConstraint(array($constraint), false); - } elseif ($this->nameConstraints[$packageName]) { - // TODO addConstraint function? - $this->nameConstraints[$packageName] = new MultiConstraint(array_merge(array($constraint), $this->nameConstraints[$packageName]->getConstraints()), false); - } - } + $loadNames[$packageName] = $constraint; + $this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null; } // all the merged constraints from install requests + fixed packages can be applied @@ -101,6 +98,13 @@ class PoolBuilder } } + // clean up loadNames for anything we manually marked loaded above + foreach ($loadNames as $name => $void) { + if (isset($this->loadedNames[$name])) { + unset($loadNames[$name]); + } + } + while (!empty($loadNames)) { foreach ($loadNames as $name => $void) { $this->loadedNames[$name] = true; diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 005047127..11aede917 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -116,6 +116,8 @@ Your requirements could not be resolved to an installable set of packages. - Root composer.json requires unstable/package 2.*, found unstable/package[2.0.0-alpha] but it does not match your minimum-stability. Problem 3 - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. + Problem 4 + - The requested package dependency/pkg could not be found in any version, there may be a typo in the package name. Problem 4 - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update. Problem 5 From bbdbb3517b59802390f74e47a499c6faf5149117 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 29 Jan 2020 18:00:56 +0100 Subject: [PATCH 283/321] PoolBuilder: Drop name constraints loop, already set earlier in same code --- src/Composer/DependencyResolver/PoolBuilder.php | 10 +--------- .../Test/Fixtures/installer/solver-problems.test | 2 -- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index dad0da6ba..3b11de76e 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -86,18 +86,10 @@ class PoolBuilder } $loadNames[$packageName] = $constraint; + // TODO do we even need to set these in name constraints or can we skip them entirely? $this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null; } - // all the merged constraints from install requests + fixed packages can be applied - // when loading package metadata already, as these are set in stone - foreach ($this->nameConstraints as $package => $constraint) { - if ($constraint !== null && array_key_exists($package, $loadNames)) { - $loadNames[$package] = $constraint; - unset($this->nameConstraints[$package]); - } - } - // clean up loadNames for anything we manually marked loaded above foreach ($loadNames as $name => $void) { if (isset($this->loadedNames[$name])) { diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 11aede917..005047127 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -116,8 +116,6 @@ Your requirements could not be resolved to an installable set of packages. - Root composer.json requires unstable/package 2.*, found unstable/package[2.0.0-alpha] but it does not match your minimum-stability. Problem 3 - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. - Problem 4 - - The requested package dependency/pkg could not be found in any version, there may be a typo in the package name. Problem 4 - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update. Problem 5 From 8a6382d78ddeb5c2497400526b3501551ecb6b35 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 12 Mar 2020 18:12:05 +0100 Subject: [PATCH 284/321] Remove unnecessary TODOs and skip EmptyConstraint like null --- src/Composer/DependencyResolver/PoolBuilder.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 3b11de76e..b570e6305 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -20,6 +20,7 @@ use Composer\Package\Version\StabilityFilter; use Composer\Repository\PlatformRepository; use Composer\Repository\RootPackageRepository; use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\EmptyConstraint; use Composer\Semver\Constraint\MultiConstraint; use Composer\EventDispatcher\EventDispatcher; use Composer\Plugin\PrePoolCreateEvent; @@ -55,7 +56,6 @@ class PoolBuilder { $loadNames = array(); foreach ($request->getFixedPackages() as $package) { - // TODO do we need to add this to nameConstraints at all? $this->nameConstraints[$package->getName()] = null; $this->loadedNames[$package->getName()] = true; @@ -86,7 +86,6 @@ class PoolBuilder } $loadNames[$packageName] = $constraint; - // TODO do we even need to set these in name constraints or can we skip them entirely? $this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null; } @@ -219,9 +218,9 @@ class PoolBuilder if (!isset($this->loadedNames[$require])) { $loadNames[$require] = null; } - if ($linkConstraint = $link->getConstraint()) { - // TODO check if linkConstraint is EmptyConstraint then set to null as well? + $linkConstraint = $link->getConstraint() + if ($linkConstraint && !($linkConstraint instanceof EmptyConstraint)) { if (!array_key_exists($require, $this->nameConstraints)) { $this->nameConstraints[$require] = new MultiConstraint(array($linkConstraint), false); } elseif ($this->nameConstraints[$require]) { From 3a9b786400756bee7a036a87159174b45c8c83ad Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 Mar 2020 11:18:41 +0100 Subject: [PATCH 285/321] Bring suggest output in line with the rest of update/install output --- src/Composer/Installer/SuggestedPackagesReporter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Installer/SuggestedPackagesReporter.php b/src/Composer/Installer/SuggestedPackagesReporter.php index c40bbd2c0..e70422d14 100644 --- a/src/Composer/Installer/SuggestedPackagesReporter.php +++ b/src/Composer/Installer/SuggestedPackagesReporter.php @@ -165,7 +165,7 @@ class SuggestedPackagesReporter { $suggestedPackages = $this->getFilteredSuggestions($installedRepo); if ($suggestedPackages) { - $this->io->writeError(count($suggestedPackages).' package suggestions were added by new dependencies, use composer suggest to see details.'); + $this->io->writeError(''.count($suggestedPackages).' package suggestions were added by new dependencies, use `composer suggest` to see details.'); } return $this; From c2d0fed06b941a919ab835be66e6a3e99b0db9cd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 Mar 2020 11:20:58 +0100 Subject: [PATCH 286/321] Tweak lock repo name --- src/Composer/Repository/LockArrayRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/LockArrayRepository.php b/src/Composer/Repository/LockArrayRepository.php index 30bbaa5b2..b485e79ec 100644 --- a/src/Composer/Repository/LockArrayRepository.php +++ b/src/Composer/Repository/LockArrayRepository.php @@ -23,7 +23,7 @@ class LockArrayRepository extends ArrayRepository { public function getRepoName() { - return 'lock '.parent::getRepoName(); + return 'lock repo'; } } From 70f23e42f3bceec973a56ca3a3732508c07b20a2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 Mar 2020 11:26:12 +0100 Subject: [PATCH 287/321] Fix tests --- tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test | 2 +- tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test b/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test index 82bf8558d..b4d1fbb09 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod-nolock.test @@ -25,7 +25,7 @@ Lock file operations: 1 install, 0 updates, 0 removals Writing lock file Installing dependencies from lock file Package operations: 1 install, 0 updates, 0 removals -1 package suggestions were added by new dependencies, use composer suggest to see details. +1 package suggestions were added by new dependencies, use `composer suggest` to see details. Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index 97e05b713..fff3e19d6 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -25,7 +25,7 @@ Lock file operations: 1 install, 0 updates, 0 removals Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals -1 package suggestions were added by new dependencies, use composer suggest to see details. +1 package suggestions were added by new dependencies, use `composer suggest` to see details. Generating autoload files --EXPECT-- From 0d2c2c044a6e18455962ff78159470c7c688b5c3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 Mar 2020 11:39:19 +0100 Subject: [PATCH 288/321] Avoid skipping feature branch detection if no branch-alias is defined at all --- src/Composer/Package/Version/VersionGuesser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 918e9706a..a34a18475 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -231,7 +231,7 @@ class VersionGuesser // ignore feature branches if they have no branch-alias or self.version is used // and find the branch they came from to use as a version instead - if ((isset($packageConfig['extra']['branch-alias']) && !isset($packageConfig['extra']['branch-alias'][$version])) + if (!isset($packageConfig['extra']['branch-alias'][$version]) || strpos(json_encode($packageConfig), '"self.version"') ) { $branch = preg_replace('{^dev-}', '', $version); From cf5513f28d350424ce6ba2ce1fbbadf6eab9be9f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 Mar 2020 11:50:35 +0100 Subject: [PATCH 289/321] Fix syntax error --- src/Composer/DependencyResolver/PoolBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index b570e6305..718f340e7 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -219,7 +219,7 @@ class PoolBuilder $loadNames[$require] = null; } - $linkConstraint = $link->getConstraint() + $linkConstraint = $link->getConstraint(); if ($linkConstraint && !($linkConstraint instanceof EmptyConstraint)) { if (!array_key_exists($require, $this->nameConstraints)) { $this->nameConstraints[$require] = new MultiConstraint(array($linkConstraint), false); From 7a05286f5527ec11bc23fc23a6b91c6a8d4cfcef Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 Mar 2020 13:33:54 +0100 Subject: [PATCH 290/321] Fix build --- tests/Composer/Test/Repository/PathRepositoryTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php index 4f7b41809..88590bffd 100644 --- a/tests/Composer/Test/Repository/PathRepositoryTest.php +++ b/tests/Composer/Test/Repository/PathRepositoryTest.php @@ -63,7 +63,7 @@ class PathRepositoryTest extends TestCase $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); $packages = $repository->getPackages(); - $this->assertEquals(1, $repository->count()); + $this->assertTrue($repository->count() >= 1); $package = $packages[0]; $this->assertEquals('test/path-unversioned', $package->getName()); @@ -85,12 +85,12 @@ class PathRepositoryTest extends TestCase $packages = $repository->getPackages(); $names = array(); - $this->assertEquals(2, $repository->count()); + $this->assertEquals(2, $repository->count() >= 2); $package = $packages[0]; $names[] = $package->getName(); - $package = $packages[1]; + $package = $packages[count($packages) - 1]; $names[] = $package->getName(); sort($names); From bbb781b1edbf50f273af96d00d4648cb3d85fa34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1chym=20Tou=C5=A1ek?= Date: Fri, 13 Mar 2020 14:40:25 +0100 Subject: [PATCH 291/321] Add --dry-run to composer require --- src/Composer/Command/RequireCommand.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index cbdfdaf9c..690250c05 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -49,6 +49,7 @@ class RequireCommand extends InitCommand new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'), + new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'), @@ -227,6 +228,7 @@ EOT $install = Installer::create($io, $composer); $install + ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) ->setPreferSource($input->getOption('prefer-source')) ->setPreferDist($input->getOption('prefer-dist')) From 3bf46a77f927e44a4043f140bca2647744eeb1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1chym=20Tou=C5=A1ek?= Date: Fri, 13 Mar 2020 18:18:13 +0100 Subject: [PATCH 292/321] Update RequireCommand.php --- src/Composer/Command/RequireCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 690250c05..67e6c720b 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -248,7 +248,7 @@ EOT ; $status = $install->run(); - if ($status !== 0) { + if ($status !== 0 || $input->getOption('dry-run')) { $this->revertComposerFile(false); } From fd63588dce15cdd9a5d96bccb2b2f57477e2a421 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 19 Mar 2020 13:27:23 +0000 Subject: [PATCH 293/321] Fixed up tests --- .../Test/Repository/PathRepositoryTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php index 88590bffd..f1c394156 100644 --- a/tests/Composer/Test/Repository/PathRepositoryTest.php +++ b/tests/Composer/Test/Repository/PathRepositoryTest.php @@ -47,7 +47,7 @@ class PathRepositoryTest extends TestCase $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); $repository->getPackages(); - $this->assertEquals(1, $repository->count()); + $this->assertSame(1, $repository->count()); $this->assertTrue($repository->hasPackage($this->getPackage('test/path-versioned', '0.0.2'))); } @@ -63,10 +63,10 @@ class PathRepositoryTest extends TestCase $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); $packages = $repository->getPackages(); - $this->assertTrue($repository->count() >= 1); + $this->assertGreaterThanOrEqual(1, $repository->count()); $package = $packages[0]; - $this->assertEquals('test/path-unversioned', $package->getName()); + $this->assertSame('test/path-unversioned', $package->getName()); $packageVersion = $package->getVersion(); $this->assertNotEmpty($packageVersion); @@ -85,7 +85,7 @@ class PathRepositoryTest extends TestCase $packages = $repository->getPackages(); $names = array(); - $this->assertEquals(2, $repository->count() >= 2); + $this->assertGreaterThanOrEqual(2, $repository->count()); $package = $packages[0]; $names[] = $package->getName(); @@ -94,7 +94,7 @@ class PathRepositoryTest extends TestCase $names[] = $package->getName(); sort($names); - $this->assertEquals(array('test/path-unversioned', 'test/path-versioned'), $names); + $this->assertSame(array('test/path-unversioned', 'test/path-versioned'), $names); } /** @@ -118,13 +118,13 @@ class PathRepositoryTest extends TestCase $repository = new PathRepository(array('url' => $relativeUrl), $ioInterface, $config); $packages = $repository->getPackages(); - $this->assertEquals(1, $repository->count()); + $this->assertSame(1, $repository->count()); $package = $packages[0]; - $this->assertEquals('test/path-versioned', $package->getName()); + $this->assertSame('test/path-versioned', $package->getName()); // Convert platform specific separators back to generic URL slashes $relativeUrl = str_replace(DIRECTORY_SEPARATOR, '/', $relativeUrl); - $this->assertEquals($relativeUrl, $package->getDistUrl()); + $this->assertSame($relativeUrl, $package->getDistUrl()); } } From da84763f034181d834daa881c64d73b29f86f9ee Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 12 Mar 2020 16:39:28 +0100 Subject: [PATCH 294/321] Move partial update handling to pool builder --- .../DependencyResolver/PoolBuilder.php | 133 +++++++++++++++++ src/Composer/DependencyResolver/Request.php | 15 ++ src/Composer/Installer.php | 137 +----------------- 3 files changed, 153 insertions(+), 132 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 718f340e7..7c17ba7ee 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -43,6 +43,8 @@ class PoolBuilder private $packages = array(); private $unacceptableFixedPackages = array(); + private $unfixList = array(); + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, EventDispatcher $eventDispatcher = null) { $this->acceptableStabilities = $acceptableStabilities; @@ -54,6 +56,16 @@ class PoolBuilder public function buildPool(array $repositories, Request $request) { + if ($request->getUpdateAllowList()) { + $this->unfixList = $request->getUpdateAllowList(); + + foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { + if (!$this->isUpdateable($lockedPackage)) { + $request->fixPackage($lockedPackage); + } + } + } + $loadNames = array(); foreach ($request->getFixedPackages() as $package) { $this->nameConstraints[$package->getName()] = null; @@ -218,6 +230,9 @@ class PoolBuilder if (!isset($this->loadedNames[$require])) { $loadNames[$require] = null; } + if (isset($request->getUpdateAllowList()[$package->getName()])) { + + } $linkConstraint = $link->getConstraint(); if ($linkConstraint && !($linkConstraint instanceof EmptyConstraint)) { @@ -235,5 +250,123 @@ class PoolBuilder return $loadNames; } + + /** + * Adds all dependencies of the update whitelist to the whitelist, too. + * + * 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 + * 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) + { + $rootRequires = array_merge($rootRequires, $rootDevRequires); + + $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('Package "' . $packageName . '" listed for update is not installed. Ignoring.'); + } + + foreach ($depPackages as $depPackage) { + $packageQueue->enqueue($depPackage); + } + + while (!$packageQueue->isEmpty()) { + $package = $packageQueue->dequeue(); + if (isset($seen[spl_object_hash($package)])) { + continue; + } + + $seen[spl_object_hash($package)] = true; + $this->updateWhitelist[$package->getName()] = true; + + if (!$this->whitelistTransitiveDependencies && !$this->whitelistAllDependencies) { + continue; + } + + $requires = $package->getRequires(); + + 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())) { + $this->io->writeError('Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.'); + continue; + } + + $packageQueue->enqueue($requirePackage); + } + } + } + } + } + + /** + * @param PackageInterface $package + * @return bool + */ + private function isUpdateable(PackageInterface $package) + { + foreach ($this->unfixList as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + if (preg_match($patternRegexp, $package->getName())) { + return true; + } + } + + return false; + } } diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index d4f1b0523..2e6cbfb57 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -27,6 +27,9 @@ class Request protected $requires = array(); protected $fixedPackages = array(); protected $unlockables = array(); + protected $updateAllowList = array(); + protected $updateAllowTransitiveDependencies = false; + protected $updateAllowTransitiveRootDependencies = false; public function __construct(LockArrayRepository $lockedRepository = null) { @@ -53,6 +56,18 @@ class Request } } + public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies, $updateAllowTransitiveRootDependencies) + { + $this->updateAllowList = $updateAllowList; + $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; + $this->updateAllowTransitiveRootDependencies = $updateAllowTransitiveRootDependencies; + } + + public function getUpdateAllowList() + { + return $this->updateAllowList; + } + public function getRequires() { return $this->requires; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 72273f11d..0187420ff 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -356,12 +356,12 @@ class Installer if (!$lockedRepository) { $this->io->writeError('Cannot update only a partial set of packages without a lock file present.', true, IOInterface::QUIET); return 1; - } + }/* $this->whitelistUpdateDependencies( $lockedRepository, $this->package->getRequires(), $this->package->getDevRequires() - ); + );*/ } $this->io->writeError('Loading composer repositories with package information'); @@ -394,14 +394,9 @@ class Installer } } - // if the updateWhitelist is enabled, packages not in it are also fixed - // to the version specified in the lock - if ($this->updateWhitelist && $lockedRepository) { - foreach ($lockedRepository->getPackages() as $lockedPackage) { - if (!$this->isUpdateable($lockedPackage)) { - $request->fixPackage($lockedPackage); - } - } + // pass the allow list into the request, so the pool builder can apply it + if ($this->updateWhitelist) { + $request->setUpdateAllowList($this->updateWhitelist, $this->whitelistTransitiveDependencies, $this->whitelistAllDependencies); } $pool = $repositorySet->createPool($request, $this->eventDispatcher); @@ -847,26 +842,6 @@ class Installer return $normalizedAliases; } - /** - * @param PackageInterface $package - * @return bool - */ - private function isUpdateable(PackageInterface $package) - { - if (!$this->updateWhitelist) { - throw new \LogicException('isUpdateable should only be called when a whitelist is present'); - } - - foreach ($this->updateWhitelist as $whiteListedPattern => $void) { - $patternRegexp = BasePackage::packageNameToRegexp($whiteListedPattern); - if (preg_match($patternRegexp, $package->getName())) { - return true; - } - } - - return false; - } - /** * @param array $links * @return array @@ -883,108 +858,6 @@ class Installer return $platformReqs; } - /** - * Adds all dependencies of the update whitelist to the whitelist, too. - * - * 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 - * 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) - { - $rootRequires = array_merge($rootRequires, $rootDevRequires); - - $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('Package "' . $packageName . '" listed for update is not installed. Ignoring.'); - } - - foreach ($depPackages as $depPackage) { - $packageQueue->enqueue($depPackage); - } - - while (!$packageQueue->isEmpty()) { - $package = $packageQueue->dequeue(); - if (isset($seen[spl_object_hash($package)])) { - continue; - } - - $seen[spl_object_hash($package)] = true; - $this->updateWhitelist[$package->getName()] = true; - - if (!$this->whitelistTransitiveDependencies && !$this->whitelistAllDependencies) { - continue; - } - - $requires = $package->getRequires(); - - 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())) { - $this->io->writeError('Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.'); - continue; - } - - $packageQueue->enqueue($requirePackage); - } - } - } - } - } - /** * Replace local repositories with InstalledArrayRepository instances * From 01fe92905aeb2928318dffe1188dc73cb4213c0f Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 27 Mar 2020 21:41:49 +0100 Subject: [PATCH 295/321] The update allow list is now generated while building the pool This reduces code complexity while making partial updates more predictable. This also allows composer require to successfully run a partial update for a new package with transitive dependency updates. --- src/Composer/Command/UpdateCommand.php | 13 +- .../DependencyResolver/PoolBuilder.php | 184 +++++++----------- src/Composer/DependencyResolver/Request.php | 26 ++- src/Composer/Installer.php | 60 ++---- src/Composer/Repository/RepositorySet.php | 5 +- .../installer/github-issues-4795.test | 4 +- ...elist-patterns-with-root-dependencies.test | 2 +- ...whitelist-warns-non-existing-patterns.test | 58 ++++++ tests/Composer/Test/InstallerTest.php | 13 +- 9 files changed, 201 insertions(+), 164 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-whitelist-warns-non-existing-patterns.test diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index a6a5dc4f7..127676d81 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\Composer; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\IO\IOInterface; use Composer\Plugin\CommandEvent; @@ -145,6 +146,13 @@ EOT $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcu = $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES; + } elseif ($input->getOption('with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_DEPENDENCIES; + } + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) @@ -158,9 +166,8 @@ EOT ->setApcuAutoloader($apcu) ->setUpdate(true) ->setUpdateMirrors($updateMirrors) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 7c17ba7ee..2037a0a9d 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -12,6 +12,7 @@ namespace Composer\DependencyResolver; +use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Package; @@ -36,32 +37,41 @@ class PoolBuilder private $rootAliases; private $rootReferences; private $eventDispatcher; + private $io; private $aliasMap = array(); private $nameConstraints = array(); private $loadedNames = array(); private $packages = 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) + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, EventDispatcher $eventDispatcher = null, IOInterface $io = null) { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; $this->eventDispatcher = $eventDispatcher; + $this->io = $io; } public function buildPool(array $repositories, Request $request) { if ($request->getUpdateAllowList()) { - $this->unfixList = $request->getUpdateAllowList(); + $this->updateAllowList = $request->getUpdateAllowList(); + $this->warnAboutNonMatchingUpdateAllowList($request); foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { - if (!$this->isUpdateable($lockedPackage)) { + if (!$this->isUpdateAllowed($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 || StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability()) ) { - $loadNames += $this->loadPackage($request, $package); + $loadNames += $this->loadPackage($request, $package, false); } else { $this->unacceptableFixedPackages[] = $package; } @@ -94,6 +104,7 @@ class PoolBuilder 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 if (isset($this->loadedNames[$packageName])) { + $this->rootRequireNotUpdated[$packageName] = true; continue; } @@ -120,7 +131,6 @@ class PoolBuilder if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) { continue; } - $result = $repository->loadPackages($loadNames, $this->acceptableStabilities, $this->stabilityFlags); foreach ($result['namesFound'] as $name) { @@ -189,7 +199,7 @@ class PoolBuilder return $pool; } - private function loadPackage(Request $request, PackageInterface $package) + private function loadPackage(Request $request, PackageInterface $package, $propagateUpdate = true) { $index = count($this->packages); $this->packages[] = $package; @@ -205,6 +215,7 @@ class PoolBuilder // apply to if (isset($this->rootReferences[$name])) { // do not modify the references on already locked packages + // TODO what about unfix on allow update? if (!$request->isFixedPackage($package)) { $package->setSourceDistReferences($this->rootReferences[$name]); } @@ -229,9 +240,16 @@ class PoolBuilder $require = $link->getTarget(); if (!isset($this->loadedNames[$require])) { $loadNames[$require] = null; - } - if (isset($request->getUpdateAllowList()[$package->getName()])) { - + // 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('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.'); + } } $linkConstraint = $link->getConstraint(); @@ -252,114 +270,23 @@ 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 - * 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 + * @return bool */ - private function whitelistUpdateDependencies($lockRepo, array $rootRequires, array $rootDevRequires) + private function isRootRequire(Request $request, $name) { - $rootRequires = array_merge($rootRequires, $rootDevRequires); - - $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('Package "' . $packageName . '" listed for update is not installed. Ignoring.'); - } - - foreach ($depPackages as $depPackage) { - $packageQueue->enqueue($depPackage); - } - - while (!$packageQueue->isEmpty()) { - $package = $packageQueue->dequeue(); - if (isset($seen[spl_object_hash($package)])) { - continue; - } - - $seen[spl_object_hash($package)] = true; - $this->updateWhitelist[$package->getName()] = true; - - if (!$this->whitelistTransitiveDependencies && !$this->whitelistAllDependencies) { - continue; - } - - $requires = $package->getRequires(); - - 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())) { - $this->io->writeError('Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.'); - continue; - } - - $packageQueue->enqueue($requirePackage); - } - } - } - } + $rootRequires = $request->getRequires(); + return isset($rootRequires[$name]); } /** - * @param PackageInterface $package + * Checks whether the update allow list allows this package in the lock file to be updated * @return bool */ - private function isUpdateable(PackageInterface $package) + private function isUpdateAllowed(PackageInterface $package) { - foreach ($this->unfixList as $pattern => $void) { + foreach ($this->updateAllowList as $pattern => $void) { $patternRegexp = BasePackage::packageNameToRegexp($pattern); if (preg_match($patternRegexp, $package->getName())) { return true; @@ -368,5 +295,42 @@ class PoolBuilder return false; } + + private function warnAboutNonMatchingUpdateAllowList(Request $request) + { + 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; + } + } + if (strpos($pattern, '*') !== false) { + $this->io->writeError('Pattern "' . $pattern . '" listed for update does not match any locked packages.'); + } else { + $this->io->writeError('Package "' . $pattern . '" listed for update is not locked.'); + } + } + } + } + + /** + * Reverts the decision to use a fixed package from lock file if a partial update with transitive dependencies + * found that this package actually needs to be updated + */ + private function unfixPackage(Request $request, $name) + { + // remove locked package by this name which was already initialized + foreach ($this->packages as $i => $loadedPackage) { + if ($loadedPackage->getName() === $name && $loadedPackage->getRepository() === $request->getLockedRepository()) { + $request->unfixPackage($loadedPackage); + unset($this->packages[$i]); + } + } + + unset($this->skippedLoad[$name]); + unset($this->loadedNames[$name]); + } } diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 2e6cbfb57..c5787b54f 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -23,13 +23,16 @@ use Composer\Semver\Constraint\ConstraintInterface; */ class Request { + const UPDATE_ONLY_LISTED = 0; + const UPDATE_TRANSITIVE_DEPENDENCIES = 1; + const UPDATE_TRANSITIVE_ROOT_DEPENDENCIES = 2; + protected $lockedRepository; protected $requires = array(); protected $fixedPackages = array(); protected $unlockables = array(); protected $updateAllowList = array(); protected $updateAllowTransitiveDependencies = false; - protected $updateAllowTransitiveRootDependencies = false; public function __construct(LockArrayRepository $lockedRepository = null) { @@ -52,15 +55,20 @@ class Request $this->fixedPackages[spl_object_hash($package)] = $package; if (!$lockable) { - $this->unlockables[] = $package; + $this->unlockables[spl_object_hash($package)] = $package; } } - public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies, $updateAllowTransitiveRootDependencies) + public function unfixPackage(PackageInterface $package) + { + unset($this->fixedPackages[spl_object_hash($package)]); + unset($this->unlockables[spl_object_hash($package)]); + } + + public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies) { $this->updateAllowList = $updateAllowList; $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; - $this->updateAllowTransitiveRootDependencies = $updateAllowTransitiveRootDependencies; } public function getUpdateAllowList() @@ -68,6 +76,16 @@ class Request return $this->updateAllowList; } + public function getUpdateAllowTransitiveDependencies() + { + return $this->updateAllowTransitiveDependencies !== self::UPDATE_ONLY_LISTED; + } + + public function getUpdateAllowTransitiveRootDependencies() + { + return $this->updateAllowTransitiveDependencies === self::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES; + } + public function getRequires() { return $this->requires; diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 0187420ff..f1d833b1e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -142,9 +142,8 @@ class Installer * @var array|null */ protected $updateMirrors = false; - protected $updateWhitelist = null; - protected $whitelistTransitiveDependencies = false; - protected $whitelistAllDependencies = false; + protected $updateAllowList = null; + protected $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; /** * @var SuggestedPackagesReporter @@ -199,8 +198,8 @@ class Installer gc_collect_cycles(); gc_disable(); - if ($this->updateWhitelist && $this->updateMirrors) { - throw new \RuntimeException("The installer options updateMirrors and updateWhitelist are mutually exclusive."); + if ($this->updateAllowList && $this->updateMirrors) { + throw new \RuntimeException("The installer options updateMirrors and updateAllowList are mutually exclusive."); } // Force update if there is no lock file present @@ -352,16 +351,11 @@ class Installer $lockedRepository = $this->locker->getLockedRepository(true); } - if ($this->updateWhitelist) { + if ($this->updateAllowList) { if (!$lockedRepository) { $this->io->writeError('Cannot update only a partial set of packages without a lock file present.', true, IOInterface::QUIET); return 1; - }/* - $this->whitelistUpdateDependencies( - $lockedRepository, - $this->package->getRequires(), - $this->package->getDevRequires() - );*/ + } } $this->io->writeError('Loading composer repositories with package information'); @@ -395,11 +389,11 @@ class Installer } // pass the allow list into the request, so the pool builder can apply it - if ($this->updateWhitelist) { - $request->setUpdateAllowList($this->updateWhitelist, $this->whitelistTransitiveDependencies, $this->whitelistAllDependencies); + if ($this->updateAllowList) { + $request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies); } - $pool = $repositorySet->createPool($request, $this->eventDispatcher); + $pool = $repositorySet->createPool($request, $this->eventDispatcher, $this->io); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -618,7 +612,7 @@ class Installer $request->requireName($link->getTarget(), $link->getConstraint()); } - $pool = $repositorySet->createPool($request, $this->eventDispatcher); + $pool = $repositorySet->createPool($request, $this->eventDispatcher, $this->io); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -1138,41 +1132,29 @@ class Installer * @param array $packages * @return Installer */ - public function setUpdateWhitelist(array $packages) + public function setUpdateAllowList(array $packages) { - $this->updateWhitelist = array_flip(array_map('strtolower', $packages)); + $this->updateAllowList = array_flip(array_map('strtolower', $packages)); return $this; } /** - * Should dependencies of whitelisted packages (but not direct dependencies) be updated? + * Should dependencies of packages marked for update be updated? * - * This will NOT whitelist any dependencies that are also directly defined - * in the root package. + * Depending on the chosen constant this will either only update the directly named packages, all transitive + * dependencies which are not root requirement or all transitive dependencies including root requirements * - * @param bool $updateTransitiveDependencies + * @param int $updateAllowTransitiveDependencies One of the UPDATE_ constants on the Request class * @return Installer */ - public function setWhitelistTransitiveDependencies($updateTransitiveDependencies = true) + public function setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) { - $this->whitelistTransitiveDependencies = (bool) $updateTransitiveDependencies; + if (!in_array($updateAllowTransitiveDependencies, array(Request::UPDATE_ONLY_LISTED, Request::UPDATE_TRANSITIVE_DEPENDENCIES, Request::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES), true)) { + throw new \RuntimeException("Invalid value for updateAllowTransitiveDependencies supplied"); + } - return $this; - } - - /** - * Should all dependencies of whitelisted packages be updated recursively? - * - * This will whitelist any dependencies of the whitelisted packages, including - * those defined in the root package. - * - * @param bool $updateAllDependencies - * @return Installer - */ - public function setWhitelistAllDependencies($updateAllDependencies = true) - { - $this->whitelistAllDependencies = (bool) $updateAllDependencies; + $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; return $this; } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index a2efbdc67..7e5d6437d 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -16,6 +16,7 @@ use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\PoolBuilder; use Composer\DependencyResolver\Request; use Composer\EventDispatcher\EventDispatcher; +use Composer\IO\IOInterface; use Composer\Package\BasePackage; use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; @@ -185,9 +186,9 @@ class RepositorySet * * @return Pool */ - public function createPool(Request $request, EventDispatcher $eventDispatcher = null) + public function createPool(Request $request, EventDispatcher $eventDispatcher = null, IOInterface $io = null) { - $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher); + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher, $io); foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index 8e5d17dfe..dc722c379 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -14,7 +14,7 @@ dependency of one the requirements that is whitelisted for update. { "name": "a/a", "version": "1.0.0" }, { "name": "a/a", "version": "1.1.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } }, - { "name": "b/b", "version": "1.1.0", "require": { "a/b": "~1.1" } } + { "name": "b/b", "version": "1.1.0", "require": { "a/a": "~1.1" } } ] } ], @@ -49,9 +49,9 @@ dependency of one the requirements that is whitelisted for update. update b/b --with-dependencies --EXPECT-OUTPUT-- -Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. Loading composer repositories with package information Updating dependencies +Dependency "a/a" 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. Nothing to modify in lock file Writing lock file Installing dependencies from lock file (including require-dev) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test index 02f544577..55a07b118 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test @@ -70,7 +70,7 @@ Update with a package whitelist only updates those packages and their dependenci "platform-dev": [] } --RUN-- -update whitelisted/pkg-* --with-dependencies +update whitelisted/pkg-* foobar --with-dependencies --EXPECT-- Upgrading dependency/pkg (1.0.0 => 1.1.0) Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-warns-non-existing-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-warns-non-existing-patterns.test new file mode 100644 index 000000000..d4d258112 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-warns-non-existing-patterns.test @@ -0,0 +1,58 @@ +--TEST-- +Verify that partial updates warn about using patterns in the argument which have no matches +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" }, + { "name": "b/b", "version": "1.1.0" } + ] + } + ], + "require": { + "a/a": "~1.0", + "b/b": "~1.0" + } +} + +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } +] + +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update b/b foo/bar baz/* --with-dependencies + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Package "foo/bar" listed for update is not locked. +Pattern "baz/*" listed for update does not match any locked packages. +Lock file operations: 0 installs, 1 update, 0 removals + - Upgrading b/b (1.0.0 => 1.1.0) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 1 update, 0 removals +Generating autoload files + +--EXPECT-- +Upgrading b/b (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 71a955748..d032dcc27 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -12,6 +12,7 @@ namespace Composer\Test; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\Console\Application; use Composer\IO\BufferIO; @@ -279,14 +280,20 @@ class InstallerTest extends TestCase $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); $packages = $filteredPackages; + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES; + } elseif ($input->getOption('with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_DEPENDENCIES; + } + $installer ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) ->setDryRun($input->getOption('dry-run')) ->setUpdateMirrors($updateMirrors) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); From 392d0abd21a716fb7b2a0b19c45c5e71646db20e Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 27 Mar 2020 22:15:04 +0100 Subject: [PATCH 296/321] Rename test files and standardize on allow list rather than whitelist --- doc/01-basic-usage.md | 2 +- doc/03-cli.md | 4 ++-- src/Composer/Cache.php | 22 +++++++++---------- src/Composer/Command/InitCommand.php | 4 ++-- src/Composer/Command/RemoveCommand.php | 5 +++-- src/Composer/Command/RequireCommand.php | 13 ++++++++--- src/Composer/Command/UpdateCommand.php | 4 ++-- .../DependencyResolver/PoolBuilder.php | 9 +++++++- src/Composer/DependencyResolver/Problem.php | 4 ++-- src/Composer/Package/BasePackage.php | 8 +++---- ...downgrades-non-allow-listed-unstable.test} | 2 +- .../Fixtures/installer/solver-problems.test | 2 +- ... => update-allow-list-locked-require.test} | 0 ...-list-patterns-with-all-dependencies.test} | 0 ...llow-list-patterns-with-dependencies.test} | 0 ...list-patterns-with-root-dependencies.test} | 0 ...w-list-patterns-without-dependencies.test} | 0 ...s.test => update-allow-list-patterns.test} | 0 ...test => update-allow-list-reads-lock.test} | 0 ... => update-allow-list-removes-unused.test} | 0 ...low-list-warns-non-existing-patterns.test} | 0 ... update-allow-list-with-dependencies.test} | 0 ...-allow-list-with-dependency-conflict.test} | 0 ...-whitelist.test => update-allow-list.test} | 0 24 files changed, 47 insertions(+), 32 deletions(-) rename tests/Composer/Test/Fixtures/installer/{partial-update-downgrades-non-whitelisted-unstable.test => partial-update-downgrades-non-allow-listed-unstable.test} (96%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-locked-require.test => update-allow-list-locked-require.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-patterns-with-all-dependencies.test => update-allow-list-patterns-with-all-dependencies.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-patterns-with-dependencies.test => update-allow-list-patterns-with-dependencies.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-patterns-with-root-dependencies.test => update-allow-list-patterns-with-root-dependencies.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-patterns-without-dependencies.test => update-allow-list-patterns-without-dependencies.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-patterns.test => update-allow-list-patterns.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-reads-lock.test => update-allow-list-reads-lock.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-removes-unused.test => update-allow-list-removes-unused.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-warns-non-existing-patterns.test => update-allow-list-warns-non-existing-patterns.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-with-dependencies.test => update-allow-list-with-dependencies.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist-with-dependency-conflict.test => update-allow-list-with-dependency-conflict.test} (100%) rename tests/Composer/Test/Fixtures/installer/{update-whitelist.test => update-allow-list.test} (100%) diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 4a3880581..aa44da021 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -159,7 +159,7 @@ php composer.phar update > if the `composer.lock` has not been updated since changes were made to the > `composer.json` that might affect dependency resolution. -If you only want to install, upgrade or remove one dependency, you can whitelist them: +If you only want to install, upgrade or remove one dependency, you can explicitly list it as an argument: ```sh php composer.phar update monolog/monolog [...] diff --git a/doc/03-cli.md b/doc/03-cli.md index e0ae7487a..725be430b 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -155,8 +155,8 @@ php composer.phar update "vendor/*" * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements. -* **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements. +* **--with-dependencies:** Update also dependencies of packages in the argument list, except those which are root requirements. +* **--with-all-dependencies:** Update also dependencies of packages in the argument list, including those which are root requirements. * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 06c6a0996..2a4e7756f 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -28,20 +28,20 @@ class Cache private $io; private $root; private $enabled = true; - private $whitelist; + private $allowlist; private $filesystem; /** * @param IOInterface $io * @param string $cacheDir location of the cache - * @param string $whitelist List of characters that are allowed in path names (used in a regex character class) + * @param string $allowlist List of characters that are allowed in path names (used in a regex character class) * @param Filesystem $filesystem optional filesystem instance */ - public function __construct(IOInterface $io, $cacheDir, $whitelist = 'a-z0-9.', Filesystem $filesystem = null) + public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null) { $this->io = $io; $this->root = rtrim($cacheDir, '/\\') . '/'; - $this->whitelist = $whitelist; + $this->allowlist = $allowlist; $this->filesystem = $filesystem ?: new Filesystem(); if (!self::isUsable($cacheDir)) { @@ -77,7 +77,7 @@ class Cache public function read($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); @@ -91,7 +91,7 @@ class Cache public function write($file, $contents) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); @@ -129,7 +129,7 @@ class Cache public function copyFrom($file, $source) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); if (!file_exists($source)) { @@ -150,7 +150,7 @@ class Cache public function copyTo($file, $target) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { try { touch($this->root . $file, filemtime($this->root . $file), time()); @@ -177,7 +177,7 @@ class Cache public function remove($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return $this->filesystem->unlink($this->root . $file); } @@ -229,7 +229,7 @@ class Cache public function sha1($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return sha1_file($this->root . $file); } @@ -241,7 +241,7 @@ class Cache public function sha256($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return hash_file('sha256', $this->root . $file); } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index d653ad61c..8f101515c 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -86,8 +86,8 @@ EOT { $io = $this->getIO(); - $whitelist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); - $options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist))); + $allowlist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); + $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist))); if (isset($options['author'])) { $options['authors'] = $this->formatAuthors($options['author']); diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index b95af9f72..0748def0c 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\Config\JsonConfigSource; +use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; @@ -179,8 +180,8 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) + ->setUpdateAllowList($packages) + ->setUpdateAllowTransitiveDependencies($input->getOption('no-update-with-dependencies') ? Request::UPDATE_ONLY_LISTED : Request::UPDATE_TRANSITIVE_DEPENDENCIES) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setRunScripts(!$input->getOption('no-scripts')) ->setDryRun($dryRun) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 97fbfe5f2..3277a1538 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\DependencyResolver\Request; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -248,6 +249,13 @@ EOT $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative'); $apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); + $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + if ($input->getOption('update-with-all-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES; + } elseif ($input->getOption('update-with-dependencies')) { + $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_DEPENDENCIES; + } + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); @@ -264,8 +272,7 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setWhitelistTransitiveDependencies($input->getOption('update-with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('update-with-all-dependencies')) + ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) @@ -275,7 +282,7 @@ EOT // if no lock is present, or the file is brand new, we do not do a // partial update as this is not supported by the Installer if (!$this->firstRequire && $composer->getConfig()->get('lock')) { - $install->setUpdateWhitelist(array_keys($requirements)); + $install->setUpdateAllowList(array_keys($requirements)); } $status = $install->run(); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 127676d81..77e166dec 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -49,8 +49,8 @@ class UpdateCommand extends BaseCommand new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), - new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of whitelisted packages to the whitelist, except those defined in root package.'), - new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist, including those defined in root package.'), + new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, except those which are root requirements.'), + new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, including those which are root requirements.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump.'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 2037a0a9d..e458c6222 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -300,12 +300,19 @@ class PoolBuilder { if ($this->io) { foreach ($this->updateAllowList as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + // update pattern matches a locked package? => all good foreach ($request->getLockedRepository()->getPackages() as $package) { - $patternRegexp = BasePackage::packageNameToRegexp($pattern); if (preg_match($patternRegexp, $package->getName())) { continue 2; } } + // update pattern matches a root require? => all good, probably a new package + foreach ($request->getRequires() as $packageName => $constraint) { + if (preg_match($patternRegexp, $packageName)) { + continue 2; + } + } if (strpos($pattern, '*') !== false) { $this->io->writeError('Pattern "' . $pattern . '" listed for update does not match any locked packages.'); } else { diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index ad9989d19..a26cc47de 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -182,7 +182,7 @@ class Problem if ($package->getName() === $packageName) { $fixedPackage = $package; if ($pool->isUnacceptableFixedPackage($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 whitelist it for update.'); + 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; } @@ -207,7 +207,7 @@ class Problem 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).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' 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.'); } } diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 0c40a3016..9ecbcb332 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -250,14 +250,14 @@ abstract class BasePackage implements PackageInterface /** * Build a regexp from a package name, expanding * globs as required * - * @param string $whiteListedPattern + * @param string $allowPattern * @param string $wrap Wrap the cleaned string by the given string * @return string */ - public static function packageNameToRegexp($whiteListedPattern, $wrap = '{^%s$}i') + public static function packageNameToRegexp($allowPattern, $wrap = '{^%s$}i') { - $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); + $cleanedAllowPattern = str_replace('\\*', '.*', preg_quote($allowPattern)); - return sprintf($wrap, $cleanedWhiteListedPattern); + return sprintf($wrap, $cleanedAllowPattern); } } diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test similarity index 96% rename from tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test rename to tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test index 8e0b18e05..25bd4a9c6 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test @@ -59,4 +59,4 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - b/unstable is fixed to 1.1.0-alpha (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you whitelist it for update. + - b/unstable is fixed to 1.1.0-alpha (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. diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 005047127..9c3d0e534 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -117,7 +117,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 3 - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. Problem 4 - - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update. + - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (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. Problem 5 - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. Problem 6 diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-warns-non-existing-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-warns-non-existing-patterns.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-allow-list.test similarity index 100% rename from tests/Composer/Test/Fixtures/installer/update-whitelist.test rename to tests/Composer/Test/Fixtures/installer/update-allow-list.test From 17b50157e49b4a18b19c408c1bb085e9053ec520 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 27 Mar 2020 22:46:54 +0100 Subject: [PATCH 297/321] Test: composer update new/plg --with-dependencies updates locked transitive deps --- ...ow-list-with-dependencies-require-new.test | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test new file mode 100644 index 000000000..24eb95538 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test @@ -0,0 +1,48 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should update locked dependencies as far as possible +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/pkg", "version": "1.1.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.1.0" }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1" } }, + { "name": "new/pkg", "version": "1.1.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Upgrading current/dep (1.0.0 => 1.1.0) +Installing new/pkg (1.0.0) From 443553423bbfa8e966df1c9299419885d4cc8a55 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 27 Mar 2020 22:59:00 +0100 Subject: [PATCH 298/321] Mark replaced packages for update when using --with-dependencies This is necessary to allow the requiring of new packages which replace packages currently locked without requiring explicitly listing them as an argument, so simplifies the composer require command --- .../DependencyResolver/PoolBuilder.php | 18 +++++++- ...with-dependencies-require-new-replace.test | 44 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index e458c6222..e715c26a5 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -215,7 +215,6 @@ class PoolBuilder // apply to if (isset($this->rootReferences[$name])) { // do not modify the references on already locked packages - // TODO what about unfix on allow update? if (!$request->isFixedPackage($package)) { $package->setSourceDistReferences($this->rootReferences[$name]); } @@ -266,6 +265,23 @@ class PoolBuilder } } + // if we're doing a partial update with deps and we're not loading an initial fixed package + // we also need to trigger an update for transitive deps which are being replaced + if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) { + foreach ($package->getReplaces() as $link) { + $replace = $link->getTarget(); + if (isset($this->loadedNames[$replace]) && isset($this->skippedLoad[$replace])) { + if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $replace)) { + $this->unfixPackage($request, $replace); + $loadNames[$replace] = null; + } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$require]) && $this->io) { + $this->updateAllowWarned[$replace] = true; + $this->io->writeError('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.'); + } + } + } + } + return $loadNames; } diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test new file mode 100644 index 000000000..8bb676e76 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test @@ -0,0 +1,44 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "current/dep": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Removing current/dep (1.0.0) +Installing new/pkg (1.0.0) From ef42d323b1e98816670cbe201a1922a610d13203 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 27 Mar 2020 23:07:02 +0100 Subject: [PATCH 299/321] Test: partial update adding a new dep on a locked package should mark it for update --- ...ist-with-dependencies-new-requirement.test | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test new file mode 100644 index 000000000..ff1257498 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test @@ -0,0 +1,49 @@ +--TEST-- +When partially updating a package to a newer version and the new version has a new requirement for a package we already have installed, mark it for update +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "root/pkg1", "version": "1.2.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "root/pkg2", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.2.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "root/pkg1": "1.*", + "root/pkg2": "1.*" + } +} +--INSTALLED-- +[ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "root/pkg1", "version": "1.0.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "root/pkg2", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update root/pkg2 --with-dependencies +--EXPECT-- +Upgrading current/dep (1.0.0 => 1.2.0) +Upgrading root/pkg2 (1.0.0 => 1.2.0) From fc40fefa6f1cb351f13d696446ef6ffdb64f3235 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 27 Mar 2020 23:13:21 +0100 Subject: [PATCH 300/321] Make sure nameConstraints is always set when loading a name --- src/Composer/DependencyResolver/PoolBuilder.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index e715c26a5..4597ec494 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -274,6 +274,8 @@ class PoolBuilder if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $replace)) { $this->unfixPackage($request, $replace); $loadNames[$replace] = null; + // TODO should we try to merge constraints here? + $this->nameConstraints[$replace] = null; } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$require]) && $this->io) { $this->updateAllowWarned[$replace] = true; $this->io->writeError('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.'); From 73251691a0be65440c722461cb38f8574cea0c25 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 28 Mar 2020 20:36:37 +0100 Subject: [PATCH 301/321] Avoid emptying the directory before extracting an archive into it, check that it is empty instead --- src/Composer/Downloader/ArchiveDownloader.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index be863f1d3..0233d5b14 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -38,7 +38,10 @@ abstract class ArchiveDownloader extends FileDownloader $this->io->writeError('Extracting archive', false); } - $this->filesystem->emptyDirectory($path); + $this->filesystem->ensureDirectoryExists($path); + if (!$this->filesystem->isDirEmpty($path)) { + throw new \RuntimeException('Expected empty path to extract '.$package.' into but directory exists: '.$path); + } $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); $fileName = $this->getFileName($package, $path); From 918768fc544198f4550e21e4f6181cb47ae63d5b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 28 Mar 2020 20:37:09 +0100 Subject: [PATCH 302/321] Make sure we get a temporary dir which is unique --- src/Composer/Downloader/ArchiveDownloader.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 0233d5b14..ce1d09691 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -43,7 +43,10 @@ abstract class ArchiveDownloader extends FileDownloader throw new \RuntimeException('Expected empty path to extract '.$package.' into but directory exists: '.$path); } - $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); + do { + $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); + } while (is_dir($temporaryDir)); + $fileName = $this->getFileName($package, $path); try { From 1b1d59ee6cbb70ef124b09ec1ff3b909e139b1b7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 28 Mar 2020 20:38:50 +0100 Subject: [PATCH 303/321] Make FileDownloader always download file first in vendor-dir/composer/$tmp instead of next to install path to avoid issues with custom installers not being loaded when downloading on first install, and use cleanup method properly --- src/Composer/Downloader/ArchiveDownloader.php | 17 ------- src/Composer/Downloader/FileDownloader.php | 18 ++++++- .../Test/Downloader/ArchiveDownloaderTest.php | 13 +++-- .../Test/Downloader/FileDownloaderTest.php | 48 ++++++++++++++++--- .../Test/Downloader/XzDownloaderTest.php | 4 +- .../Test/Downloader/ZipDownloaderTest.php | 2 +- 6 files changed, 70 insertions(+), 32 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index ce1d09691..318e73c6f 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -91,32 +91,15 @@ abstract class ArchiveDownloader extends FileDownloader } $this->filesystem->removeDirectory($temporaryDir); - if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { - $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/'); - } - if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { - $this->filesystem->removeDirectory($this->config->get('vendor-dir')); - } } catch (\Exception $e) { // clean up $this->filesystem->removeDirectory($path); $this->filesystem->removeDirectory($temporaryDir); - if (file_exists($fileName)) { - $this->filesystem->unlink($fileName); - } throw $e; } } - /** - * {@inheritdoc} - */ - protected function getFileName(PackageInterface $package, $path) - { - return rtrim($path.'_'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); - } - /** * Extract file to directory * diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 357c5b59d..f94ec228b 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -101,8 +101,9 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface ); } - $this->filesystem->ensureDirectoryExists($path); $fileName = $this->getFileName($package, $path); + $this->filesystem->ensureDirectoryExists($path); + $this->filesystem->ensureDirectoryExists(dirname($fileName)); $io = $this->io; $cache = $this->cache; @@ -234,6 +235,19 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface */ public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) { + $fileName = $this->getFileName($package, $path); + if (file_exists($fileName)) { + $this->filesystem->unlink($fileName); + } + if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { + $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/'); + } + if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { + $this->filesystem->removeDirectory($this->config->get('vendor-dir')); + } + if ($this->filesystem->isDirEmpty($path)) { + $this->filesystem->removeDirectory($path); + } } /** @@ -302,7 +316,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface */ protected function getFileName(PackageInterface $package, $path) { - return $path.'_'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); + return rtrim($this->config->get('vendor-dir').'/composer/'.md5($package.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); } /** diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php index 360539859..793c5eab5 100644 --- a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -16,6 +16,8 @@ use Composer\Test\TestCase; class ArchiveDownloaderTest extends TestCase { + protected $config; + public function testGetFileName() { $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); @@ -28,8 +30,13 @@ class ArchiveDownloaderTest extends TestCase $method = new \ReflectionMethod($downloader, 'getFileName'); $method->setAccessible(true); + $this->config->expects($this->any()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue('/vendor')); + $first = $method->invoke($downloader, $packageMock, '/path'); - $this->assertRegExp('#/path_[a-z0-9]+\.js#', $first); + $this->assertRegExp('#/vendor/composer/[a-z0-9]+\.js#', $first); $this->assertSame($first, $method->invoke($downloader, $packageMock, '/path')); } @@ -158,8 +165,8 @@ class ArchiveDownloaderTest extends TestCase 'Composer\Downloader\ArchiveDownloader', array( $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), - $config = $this->getMockBuilder('Composer\Config')->getMock(), - new \Composer\Util\HttpDownloader($io, $config), + $this->config = $this->getMockBuilder('Composer\Config')->getMock(), + new \Composer\Util\HttpDownloader($io, $this->config), ) ); } diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index 2366df770..af5a9da11 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -21,11 +21,12 @@ use Composer\Util\Loop; class FileDownloaderTest extends TestCase { private $httpDownloader; + private $config; protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $httpDownloader = null, $filesystem = null) { $io = $io ?: $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $config = $config ?: $this->getMockBuilder('Composer\Config')->getMock(); + $this->config = $config ?: $this->getMockBuilder('Composer\Config')->getMock(); $httpDownloader = $httpDownloader ?: $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(); $httpDownloader ->expects($this->any()) @@ -33,7 +34,7 @@ class FileDownloaderTest extends TestCase ->will($this->returnValue(\React\Promise\resolve(new Response(array('url' => 'http://example.org/'), 200, array(), 'file~')))); $this->httpDownloader = $httpDownloader; - return new FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $filesystem); + return new FileDownloader($io, $this->config, $httpDownloader, $eventDispatcher, $cache, $filesystem); } /** @@ -54,11 +55,11 @@ class FileDownloaderTest extends TestCase public function testDownloadToExistingFile() { $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); - $packageMock->expects($this->once()) + $packageMock->expects($this->any()) ->method('getDistUrl') ->will($this->returnValue('url')) ; - $packageMock->expects($this->once()) + $packageMock->expects($this->any()) ->method('getDistUrls') ->will($this->returnValue(array('url'))) ; @@ -93,7 +94,12 @@ class FileDownloaderTest extends TestCase $method = new \ReflectionMethod($downloader, 'getFileName'); $method->setAccessible(true); - $this->assertEquals('/path_script.js', $method->invoke($downloader, $packageMock, '/path')); + $this->config->expects($this->once()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue('/vendor')); + + $this->assertRegExp('#/vendor/composer/[a-z0-9]+\.js#', $method->invoke($downloader, $packageMock, '/path')); } public function testDownloadButFileIsUnsaved() @@ -126,6 +132,12 @@ class FileDownloaderTest extends TestCase ; $downloader = $this->getDownloader($ioMock); + + $this->config->expects($this->once()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue($path.'/vendor')); + try { $promise = $downloader->download($packageMock, $path); $loop = new Loop($this->httpDownloader); @@ -199,8 +211,18 @@ class FileDownloaderTest extends TestCase $path = $this->getUniqueTmpDirectory(); $downloader = $this->getDownloader(null, null, null, null, null, $filesystem); + // make sure the file expected to be downloaded is on disk already - touch($path.'_script.js'); + $this->config->expects($this->any()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue($path.'/vendor')); + + $method = new \ReflectionMethod($downloader, 'getFileName'); + $method->setAccessible(true); + $dlFile = $method->invoke($downloader, $packageMock, $path); + mkdir(dirname($dlFile), 0777, true); + touch($dlFile); try { $promise = $downloader->download($packageMock, $path); @@ -255,13 +277,25 @@ class FileDownloaderTest extends TestCase ->with($this->stringContains('Downgrading')); $path = $this->getUniqueTmpDirectory(); - touch($path.'_script.js'); $filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); $filesystem->expects($this->once()) ->method('removeDirectory') ->will($this->returnValue(true)); $downloader = $this->getDownloader($ioMock, null, null, null, null, $filesystem); + + // make sure the file expected to be downloaded is on disk already + $this->config->expects($this->any()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue($path.'/vendor')); + + $method = new \ReflectionMethod($downloader, 'getFileName'); + $method->setAccessible(true); + $dlFile = $method->invoke($downloader, $newPackage, $path); + mkdir(dirname($dlFile), 0777, true); + touch($dlFile); + $promise = $downloader->download($newPackage, $path, $oldPackage); $loop = new Loop($this->httpDownloader); $loop->wait(array($promise)); diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index 4c2fdb2af..f770b0d35 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -70,10 +70,10 @@ class XzDownloaderTest extends TestCase $downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null); try { - $promise = $downloader->download($packageMock, $this->testDir); + $promise = $downloader->download($packageMock, $this->testDir.'/install-path'); $loop = new Loop($httpDownloader); $loop->wait(array($promise)); - $downloader->install($packageMock, $this->testDir); + $downloader->install($packageMock, $this->testDir.'/install-path'); $this->fail('Download of invalid tarball should throw an exception'); } catch (\RuntimeException $e) { diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index eb57e67df..e3bbe45a8 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -69,7 +69,7 @@ class ZipDownloaderTest extends TestCase $this->markTestSkipped('zip extension missing'); } - $this->config->expects($this->at(0)) + $this->config->expects($this->any()) ->method('get') ->with('vendor-dir') ->will($this->returnValue($this->testDir)); From 6e45a53e76fe7d3fc85cdcb8c7bf873e7c9d83cf Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 28 Mar 2020 20:39:11 +0100 Subject: [PATCH 304/321] Add support for relative paths in handling of install-path for the installed.json --- src/Composer/Repository/FilesystemRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index 0b6563dd6..3bebfbdb3 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -96,7 +96,7 @@ class FilesystemRepository extends WritableArrayRepository foreach ($this->getCanonicalPackages() as $package) { $pkgArray = $dumper->dump($package); $path = $installationManager->getInstallPath($package); - $pkgArray['install-path'] = ('' !== $path && null !== $path) ? $fs->findShortestPath($repoDir, $path, true) : null; + $pkgArray['install-path'] = ('' !== $path && null !== $path) ? $fs->findShortestPath($repoDir, $fs->isAbsolutePath($path) ? $path : getcwd() . '/' . $path, true) : null; $data['packages'][] = $pkgArray; } From 800491175b7c3db0eec19c81997d0e51fdd47771 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 28 Mar 2020 20:50:49 +0100 Subject: [PATCH 305/321] Avoid failing cleanup when a package gets uninstalled --- src/Composer/Downloader/FileDownloader.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index f94ec228b..bce1b7133 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -239,13 +239,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface if (file_exists($fileName)) { $this->filesystem->unlink($fileName); } - if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { + if (is_dir($path) && $this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/'); } - if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { + if (is_dir($path) && $this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { $this->filesystem->removeDirectory($this->config->get('vendor-dir')); } - if ($this->filesystem->isDirEmpty($path)) { + if (is_dir($path) && $this->filesystem->isDirEmpty($path)) { $this->filesystem->removeDirectory($path); } } From 9fb09049ff3b17bc6298d18bba92a79b51892408 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Mon, 30 Mar 2020 16:21:27 +0200 Subject: [PATCH 306/321] Rename and document constants for partial update behavior --- src/Composer/Command/RemoveCommand.php | 2 +- src/Composer/Command/RequireCommand.php | 4 ++-- src/Composer/Command/UpdateCommand.php | 4 ++-- src/Composer/DependencyResolver/Request.php | 19 ++++++++++++++++--- src/Composer/Installer.php | 2 +- tests/Composer/Test/InstallerTest.php | 4 ++-- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 0748def0c..7ae8abbe2 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -181,7 +181,7 @@ EOT ->setApcuAutoloader($apcu) ->setUpdate(true) ->setUpdateAllowList($packages) - ->setUpdateAllowTransitiveDependencies($input->getOption('no-update-with-dependencies') ? Request::UPDATE_ONLY_LISTED : Request::UPDATE_TRANSITIVE_DEPENDENCIES) + ->setUpdateAllowTransitiveDependencies($input->getOption('no-update-with-dependencies') ? Request::UPDATE_ONLY_LISTED : Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setRunScripts(!$input->getOption('no-scripts')) ->setDryRun($dryRun) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 3277a1538..357127925 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -251,9 +251,9 @@ EOT $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; if ($input->getOption('update-with-all-dependencies')) { - $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES; + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; } elseif ($input->getOption('update-with-dependencies')) { - $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_DEPENDENCIES; + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; } $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 77e166dec..840f7ae28 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -148,9 +148,9 @@ EOT $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; if ($input->getOption('with-all-dependencies')) { - $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES; + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; } elseif ($input->getOption('with-dependencies')) { - $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_DEPENDENCIES; + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; } $install diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index c5787b54f..5782c3ff1 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -23,9 +23,22 @@ use Composer\Semver\Constraint\ConstraintInterface; */ class Request { + /** + * Identifies a partial update for listed packages only, all dependencies will remain at locked versions + */ const UPDATE_ONLY_LISTED = 0; - const UPDATE_TRANSITIVE_DEPENDENCIES = 1; - const UPDATE_TRANSITIVE_ROOT_DEPENDENCIES = 2; + + /** + * Identifies a partial update for listed packages and recursively all their dependencies, however dependencies + * also directly required by the root composer.json and their dependencies will remain at the locked version. + */ + const UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE = 1; + + /** + * Identifies a partial update for listed packages and recursively all their dependencies, even dependencies + * also directly required by the root composer.json will be updated. + */ + const UPDATE_LISTED_WITH_TRANSITIVE_DEPS = 2; protected $lockedRepository; protected $requires = array(); @@ -83,7 +96,7 @@ class Request public function getUpdateAllowTransitiveRootDependencies() { - return $this->updateAllowTransitiveDependencies === self::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES; + return $this->updateAllowTransitiveDependencies === self::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; } public function getRequires() diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index f1d833b1e..500a9a827 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -1150,7 +1150,7 @@ class Installer */ public function setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) { - if (!in_array($updateAllowTransitiveDependencies, array(Request::UPDATE_ONLY_LISTED, Request::UPDATE_TRANSITIVE_DEPENDENCIES, Request::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES), true)) { + if (!in_array($updateAllowTransitiveDependencies, array(Request::UPDATE_ONLY_LISTED, Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE, Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS), true)) { throw new \RuntimeException("Invalid value for updateAllowTransitiveDependencies supplied"); } diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index d032dcc27..3c6c35de6 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -282,9 +282,9 @@ class InstallerTest extends TestCase $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; if ($input->getOption('with-all-dependencies')) { - $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_ROOT_DEPENDENCIES; + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; } elseif ($input->getOption('with-dependencies')) { - $updateAllowTransitiveDependencies = Request::UPDATE_TRANSITIVE_DEPENDENCIES; + $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; } $installer From 71c3c63b545b1fd812d5883146b651f40ba960cb Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Mon, 30 Mar 2020 16:37:31 +0200 Subject: [PATCH 307/321] Remove unused variable assignment --- src/Composer/DependencyResolver/PoolBuilder.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 4597ec494..06c50bdeb 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -104,7 +104,6 @@ class PoolBuilder 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 if (isset($this->loadedNames[$packageName])) { - $this->rootRequireNotUpdated[$packageName] = true; continue; } From c270d3cfa6c677bd92db046bf6ff58018e2e42be Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 1 Apr 2020 15:27:51 +0200 Subject: [PATCH 308/321] PoolBuilder: make io non-nullable, NullIO can be used instead --- .../DependencyResolver/PoolBuilder.php | 40 +++++++++---------- src/Composer/Installer.php | 4 +- src/Composer/Repository/RepositorySet.php | 7 ++-- .../DependencyResolver/PoolBuilderTest.php | 2 +- .../Test/DependencyResolver/SolverTest.php | 5 ++- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 06c50bdeb..36c991816 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -48,7 +48,7 @@ class PoolBuilder private $skippedLoad = array(); private $updateAllowWarned = 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, IOInterface $io = null, EventDispatcher $eventDispatcher = null) { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; @@ -244,7 +244,7 @@ class PoolBuilder 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) { + } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $require) && !isset($this->updateAllowWarned[$require])) { $this->updateAllowWarned[$require] = true; $this->io->writeError('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.'); } @@ -275,7 +275,7 @@ class PoolBuilder $loadNames[$replace] = null; // TODO should we try to merge constraints here? $this->nameConstraints[$replace] = null; - } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$require]) && $this->io) { + } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$require])) { $this->updateAllowWarned[$replace] = true; $this->io->writeError('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.'); } @@ -315,27 +315,25 @@ class PoolBuilder private function warnAboutNonMatchingUpdateAllowList(Request $request) { - if ($this->io) { - foreach ($this->updateAllowList as $pattern => $void) { - $patternRegexp = BasePackage::packageNameToRegexp($pattern); - // update pattern matches a locked package? => all good - foreach ($request->getLockedRepository()->getPackages() as $package) { - if (preg_match($patternRegexp, $package->getName())) { - continue 2; - } + foreach ($this->updateAllowList as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + // update pattern matches a locked package? => all good + foreach ($request->getLockedRepository()->getPackages() as $package) { + if (preg_match($patternRegexp, $package->getName())) { + continue 2; } - // update pattern matches a root require? => all good, probably a new package - foreach ($request->getRequires() as $packageName => $constraint) { - if (preg_match($patternRegexp, $packageName)) { - continue 2; - } - } - if (strpos($pattern, '*') !== false) { - $this->io->writeError('Pattern "' . $pattern . '" listed for update does not match any locked packages.'); - } else { - $this->io->writeError('Package "' . $pattern . '" listed for update is not locked.'); + } + // update pattern matches a root require? => all good, probably a new package + foreach ($request->getRequires() as $packageName => $constraint) { + if (preg_match($patternRegexp, $packageName)) { + continue 2; } } + if (strpos($pattern, '*') !== false) { + $this->io->writeError('Pattern "' . $pattern . '" listed for update does not match any locked packages.'); + } else { + $this->io->writeError('Package "' . $pattern . '" listed for update is not locked.'); + } } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 500a9a827..3937a15b0 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -393,7 +393,7 @@ class Installer $request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies); } - $pool = $repositorySet->createPool($request, $this->eventDispatcher, $this->io); + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); @@ -612,7 +612,7 @@ class Installer $request->requireName($link->getTarget(), $link->getConstraint()); } - $pool = $repositorySet->createPool($request, $this->eventDispatcher, $this->io); + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); // solve dependencies $solver = new Solver($policy, $pool, $this->io, $repositorySet); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 7e5d6437d..b0489e99b 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -17,6 +17,7 @@ use Composer\DependencyResolver\PoolBuilder; use Composer\DependencyResolver\Request; use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; +use Composer\IO\NullIO; use Composer\Package\BasePackage; use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; @@ -186,9 +187,9 @@ class RepositorySet * * @return Pool */ - public function createPool(Request $request, EventDispatcher $eventDispatcher = null, IOInterface $io = null) + public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null) { - $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $eventDispatcher, $io); + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher); foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { @@ -237,6 +238,6 @@ class RepositorySet $request->requireName($packageName); } - return $this->createPool($request); + return $this->createPool($request, new NullIO()); } } diff --git a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php index c55431b6e..f51661db6 100644 --- a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php @@ -89,7 +89,7 @@ class PoolBuilderTest extends TestCase $request->fixPackage($loadPackage($fixedPackage)); } - $pool = $repositorySet->createPool($request); + $pool = $repositorySet->createPool($request, new NullIO()); for ($i = 1, $count = count($pool); $i <= $count; $i++) { $result[] = $pool->packageById($i); } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 46d882e9f..3090d9d83 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -890,8 +890,9 @@ class SolverTest extends TestCase protected function createSolver() { - $this->pool = $this->repoSet->createPool($this->request); - $this->solver = new Solver($this->policy, $this->pool, new NullIO()); + $io = new NullIO(); + $this->pool = $this->repoSet->createPool($this->request, $io); + $this->solver = new Solver($this->policy, $this->pool, $io); } protected function checkSolverResult(array $expected) From 14000e75758e73f03fb7750ca4f2c81699013822 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 1 Apr 2020 16:29:24 +0200 Subject: [PATCH 309/321] PoolBuilder: Ensure alias references get removed when unfixing a locked package Use the last key + 1 for an index instead of count, since we unset elements somtimes --- .../DependencyResolver/PoolBuilder.php | 4 +- ...te-allow-list-with-dependencies-alias.test | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 36c991816..9dbe7ce9a 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -200,7 +200,8 @@ class PoolBuilder private function loadPackage(Request $request, PackageInterface $package, $propagateUpdate = true) { - $index = count($this->packages); + end($this->packages); + $index = key($this->packages) + 1; $this->packages[] = $package; if ($package instanceof AliasPackage) { @@ -348,6 +349,7 @@ class PoolBuilder if ($loadedPackage->getName() === $name && $loadedPackage->getRepository() === $request->getLockedRepository()) { $request->unfixPackage($loadedPackage); unset($this->packages[$i]); + unset($this->aliasMap[spl_object_hash($loadedPackage)]); } } diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test new file mode 100644 index 000000000..24eb95538 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test @@ -0,0 +1,48 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should update locked dependencies as far as possible +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/pkg", "version": "1.1.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "current/dep", "version": "1.1.0" }, + { "name": "current/dep", "version": "1.2.0" }, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1" } }, + { "name": "new/pkg", "version": "1.1.0", "require": { "current/dep": "^1.2" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Upgrading current/dep (1.0.0 => 1.1.0) +Installing new/pkg (1.0.0) From 2dfbf3692a2f5535ef48384aa83710d73113257a Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 1 Apr 2020 16:42:12 +0200 Subject: [PATCH 310/321] PoolBuilder: Remove null default for io --- src/Composer/DependencyResolver/PoolBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 9dbe7ce9a..69e6c8a4a 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -48,7 +48,7 @@ class PoolBuilder private $skippedLoad = array(); private $updateAllowWarned = array(); - public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io = null, EventDispatcher $eventDispatcher = null) + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null) { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; From 613450e58a90538e92de4cf7be62a1fa11afd917 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 00:08:53 +0200 Subject: [PATCH 311/321] PoolBuilder: properly clean up alias packages when unfixing packages Prevent aliases to be duplicated: no need to apply root aliases from composer.json on packages currently locked, they should have their aliases in the lock file, otherwise request an update. --- .../DependencyResolver/PoolBuilder.php | 22 +++-- ...te-allow-list-with-dependencies-alias.test | 84 ++++++++++++++++--- 2 files changed, 88 insertions(+), 18 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 69e6c8a4a..55f3b4c9e 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -220,7 +220,9 @@ class PoolBuilder } } - if (isset($this->rootAliases[$name][$package->getVersion()])) { + // 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 ($propagateUpdate && isset($this->rootAliases[$name][$package->getVersion()])) { $alias = $this->rootAliases[$name][$package->getVersion()]; if ($package instanceof AliasPackage) { $basePackage = $package->getAliasOf(); @@ -345,11 +347,19 @@ class PoolBuilder private function unfixPackage(Request $request, $name) { // remove locked package by this name which was already initialized - foreach ($this->packages as $i => $loadedPackage) { - if ($loadedPackage->getName() === $name && $loadedPackage->getRepository() === $request->getLockedRepository()) { - $request->unfixPackage($loadedPackage); - unset($this->packages[$i]); - unset($this->aliasMap[spl_object_hash($loadedPackage)]); + foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { + if (!($lockedPackage instanceof AliasPackage) && $lockedPackage->getName() === $name) { + if (false !== $index = array_search($lockedPackage, $this->packages, true)) { + $request->unfixPackage($lockedPackage); + unset($this->packages[$index]); + if (isset($this->aliasMap[spl_object_hash($lockedPackage)])) { + foreach ($this->aliasMap[spl_object_hash($lockedPackage)] as $aliasIndex => $aliasPackage) { + $request->unfixPackage($aliasPackage); + unset($this->packages[$aliasIndex]); + } + unset($this->aliasMap[spl_object_hash($lockedPackage)]); + } + } } } diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test index 24eb95538..b4685bb09 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test @@ -7,33 +7,52 @@ Require a new package in the composer.json and updating with its name as an argu "type": "package", "package": [ { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, - { "name": "current/pkg", "version": "1.1.0", "require": { "current/dep": "^1.0" } }, + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, { "name": "current/dep", "version": "1.0.0" }, - { "name": "current/dep", "version": "1.1.0" }, + { "name": "current/dep", "version": "1.1.0", "require": {"current/dep2": "*"} }, { "name": "current/dep", "version": "1.2.0" }, - { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1" } }, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "*"} }, { "name": "new/pkg", "version": "1.1.0", "require": { "current/dep": "^1.2" } } ] } ], "require": { - "current/pkg": "1.*", + "current/dep": "dev-master as 1.1.0", + "current/dep2": "dev-master as 1.1.0", + "current/pkg": "1.0.0 as 2.0.0", "new/pkg": "1.*" - } + }, + "minimum-stability": "dev" } --INSTALLED-- [ - { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, - { "name": "current/dep", "version": "1.0.0" } + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } } ] --LOCK-- { "packages": [ - { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } }, - { "name": "current/dep", "version": "1.0.0" } + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } } ], "packages-dev": [], - "aliases": [], + "aliases": [ + { + "alias": "1.1.0", + "alias_normalized": "1.1.0.0", + "version": "dev-master", + "package": "current/dep" + }, + { + "alias": "1.1.0", + "alias_normalized": "1.1.0.0", + "version": "dev-master", + "package": "current/dep2" + } + ], "minimum-stability": "dev", "stability-flags": [], "prefer-stable": false, @@ -42,7 +61,48 @@ Require a new package in the composer.json and updating with its name as an argu "platform-dev": [] } --RUN-- -update new/pkg --with-dependencies +update new/pkg --with-all-dependencies +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" }, "type": "library"}, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "*"}, "type": "library"} + ], + "packages-dev": [], + "aliases": [ + { + "alias": "1.1.0", + "alias_normalized": "1.1.0.0", + "version": "dev-master", + "package": "current/dep" + }, + { + "alias": "1.1.0", + "alias_normalized": "1.1.0.0", + "version": "dev-master", + "package": "current/dep2" + }, + { + "alias": "2.0.0", + "alias_normalized": "2.0.0.0", + "version": "1.0.0.0", + "package": "current/pkg" + } + ], + "minimum-stability": "dev", + "stability-flags": { + "current/dep": 20, + "current/dep2": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --EXPECT-- -Upgrading current/dep (1.0.0 => 1.1.0) +Marking current/dep (1.1.0) as installed, alias of current/dep (dev-master) +Marking current/pkg (2.0.0) as installed, alias of current/pkg (1.0.0) +Marking current/dep2 (1.1.0) as installed, alias of current/dep2 (dev-master) Installing new/pkg (1.0.0) From 5ad93959cf674e85296e64714ff0d2006199c64f Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 00:54:49 +0200 Subject: [PATCH 312/321] PoolBuilder: On partial update of a new dep with mutual replace, unfix replacer Test also verifies provider does not get uninstalled in partial update for another package name --- .../DependencyResolver/PoolBuilder.php | 14 ++++-- ...pendencies-require-new-replace-mutual.test | 50 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 55f3b4c9e..305ad16b6 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -67,10 +67,11 @@ class PoolBuilder foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { if (!$this->isUpdateAllowed($lockedPackage)) { $request->fixPackage($lockedPackage); + $lockedName = $lockedPackage->getName(); // remember which packages we skipped loading remote content for in this partial update - $this->skippedLoad[$lockedPackage->getName()] = true; + $this->skippedLoad[$lockedPackage->getName()] = $lockedName; foreach ($lockedPackage->getReplaces() as $link) { - $this->skippedLoad[$link->getTarget()] = true; + $this->skippedLoad[$link->getTarget()] = $lockedName; } } } @@ -244,7 +245,7 @@ class PoolBuilder // 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)) { + if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$require])) { $this->unfixPackage($request, $require); $loadNames[$require] = null; } elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $require) && !isset($this->updateAllowWarned[$require])) { @@ -273,7 +274,7 @@ class PoolBuilder foreach ($package->getReplaces() as $link) { $replace = $link->getTarget(); if (isset($this->loadedNames[$replace]) && isset($this->skippedLoad[$replace])) { - if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $replace)) { + if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$replace])) { $this->unfixPackage($request, $replace); $loadNames[$replace] = null; // TODO should we try to merge constraints here? @@ -363,6 +364,11 @@ class PoolBuilder } } + // if we unfixed a replaced package name, we also need to unfix the replacer itself + if ($this->skippedLoad[$name] !== $name) { + $this->unfixPackage($request, $this->skippedLoad[$name]); + } + unset($this->skippedLoad[$name]); unset($this->loadedNames[$name]); } diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test new file mode 100644 index 000000000..0cb5ad97f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test @@ -0,0 +1,50 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*", "mutual/target-provide": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } }, + { "name": "new/pkg-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*", + "new/pkg-provide": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "mutual/target": "*" } }, + { "name": "current/dep", "version": "1.0.0", "replace": { "mutual/target": "1.0.0" } }, + { "name": "current/dep-provide", "version": "1.0.0", "provide": { "mutual/target-provide": "1.0.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg --with-dependencies +--EXPECT-- +Removing current/dep (1.0.0) +Installing new/pkg (1.0.0) +Installing new/pkg-provide (1.0.0) From f7b5cbd1fc8fdaf18237e6d9f799fdc6e9e9c327 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 01:12:34 +0200 Subject: [PATCH 313/321] Lock file should not contain new aliases for packages which were not updated --- .../DependencyResolver/LockTransaction.php | 18 +++++++++ src/Composer/Installer.php | 2 +- ...te-allow-list-with-dependencies-alias.test | 39 +++++++------------ 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index b74cfbc78..dbbad01d6 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -112,4 +112,22 @@ class LockTransaction extends Transaction return $packages; } + + /** + * Checks which of the given aliases from composer.json are actually in use for the lock file + */ + public function getAliases($aliases) + { + $usedAliases = array(); + + foreach ($this->resultPackages['all'] as $package) { + if ($package instanceof AliasPackage) { + if (isset($aliases[$package->getName()])) { + $usedAliases[$package->getName()] = $aliases[$package->getName()]; + } + } + } + + return $usedAliases; + } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 3937a15b0..15cfb78e1 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -497,7 +497,7 @@ class Installer $lockTransaction->getNewLockPackages(true, $this->updateMirrors), $platformReqs, $platformDevReqs, - $aliases, + $lockTransaction->getAliases($aliases), $this->package->getMinimumStability(), $this->package->getStabilityFlags(), $this->preferStable || $this->package->getPreferStable(), diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test index b4685bb09..2b68c6c69 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test @@ -1,5 +1,5 @@ --TEST-- -Require a new package in the composer.json and updating with its name as an argument and with-dependencies should update locked dependencies as far as possible +Verify that a partial update with deps correctly keeps track of all aliases. --COMPOSER-- { "repositories": [ @@ -11,15 +11,16 @@ Require a new package in the composer.json and updating with its name as an argu { "name": "current/dep", "version": "1.0.0" }, { "name": "current/dep", "version": "1.1.0", "require": {"current/dep2": "*"} }, { "name": "current/dep", "version": "1.2.0" }, - { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, - { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "*"} }, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "2.x-dev"}}}, + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "^1.1"} }, { "name": "new/pkg", "version": "1.1.0", "require": { "current/dep": "^1.2" } } ] } ], "require": { "current/dep": "dev-master as 1.1.0", - "current/dep2": "dev-master as 1.1.0", + "current/dep2": "dev-master as 1.1.2", "current/pkg": "1.0.0 as 2.0.0", "new/pkg": "1.*" }, @@ -28,14 +29,14 @@ Require a new package in the composer.json and updating with its name as an argu --INSTALLED-- [ { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, - { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}}, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}}, { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } } ] --LOCK-- { "packages": [ { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, - { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/dep2", "version": "dev-foo", "extra": {"branch-alias": {"dev-foo": "1.0.x-dev"}}, "type": "library"}, { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" } } ], "packages-dev": [], @@ -45,12 +46,6 @@ Require a new package in the composer.json and updating with its name as an argu "alias_normalized": "1.1.0.0", "version": "dev-master", "package": "current/dep" - }, - { - "alias": "1.1.0", - "alias_normalized": "1.1.0.0", - "version": "dev-master", - "package": "current/dep2" } ], "minimum-stability": "dev", @@ -66,9 +61,9 @@ update new/pkg --with-all-dependencies { "packages": [ { "name": "current/dep", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, - { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "1.0.x-dev"}}, "type": "library"}, + { "name": "current/dep2", "version": "dev-master", "extra": {"branch-alias": {"dev-master": "2.x-dev"}}, "type": "library"}, { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "<1.2.0" }, "type": "library"}, - { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "*"}, "type": "library"} + { "name": "new/pkg", "version": "1.0.0", "require": { "current/dep": "^1.1", "current/dep2": "^1.1"}, "type": "library"} ], "packages-dev": [], "aliases": [ @@ -79,16 +74,10 @@ update new/pkg --with-all-dependencies "package": "current/dep" }, { - "alias": "1.1.0", - "alias_normalized": "1.1.0.0", + "alias": "1.1.2", + "alias_normalized": "1.1.2.0", "version": "dev-master", "package": "current/dep2" - }, - { - "alias": "2.0.0", - "alias_normalized": "2.0.0.0", - "version": "1.0.0.0", - "package": "current/pkg" } ], "minimum-stability": "dev", @@ -103,6 +92,8 @@ update new/pkg --with-all-dependencies } --EXPECT-- Marking current/dep (1.1.0) as installed, alias of current/dep (dev-master) -Marking current/pkg (2.0.0) as installed, alias of current/pkg (1.0.0) -Marking current/dep2 (1.1.0) as installed, alias of current/dep2 (dev-master) +Upgrading current/dep2 (dev-foo => dev-master) +Marking current/dep2 (1.1.2) as installed, alias of current/dep2 (dev-master) +Marking current/dep2 (2.x-dev) as installed, alias of current/dep2 (dev-master) Installing new/pkg (1.0.0) +Marking current/dep2 (1.0.x-dev) as uninstalled, alias of current/dep2 (dev-foo) From 1b55b466fb79ce5dd01852d71a942abcf487e5f9 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 14:19:36 +0200 Subject: [PATCH 314/321] require: Add with-dependencies and with-all-dependencies as alternative option names --- src/Composer/Command/RequireCommand.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 357127925..008b5dff1 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -62,6 +62,8 @@ class RequireCommand extends InitCommand new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), new InputOption('update-with-dependencies', null, InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated, except those that are root requirements.'), new InputOption('update-with-all-dependencies', null, InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'), + new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-dependencies'), + new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'), new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'), @@ -250,9 +252,9 @@ EOT $apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; - if ($input->getOption('update-with-all-dependencies')) { + if ($input->getOption('update-with-all-dependencies') || $input->getOption('with-all-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; - } elseif ($input->getOption('update-with-dependencies')) { + } elseif ($input->getOption('update-with-dependencies') || $input->getOption('with-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; } From 4e7ff690c6311dcf1e8882194e626014fc2bcccb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 2 Apr 2020 14:39:53 +0200 Subject: [PATCH 315/321] Revert lock file as well when a require command operation failed to complete --- src/Composer/Command/RequireCommand.php | 18 +++++++++++++++++- src/Composer/Factory.php | 11 ++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 357127925..23076f2a5 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -42,6 +42,10 @@ class RequireCommand extends InitCommand private $json; private $file; private $composerBackup; + /** @var string file name */ + private $lock; + /** @var ?string contents before modification if the lock file exists */ + private $lockBackup; protected function configure() { @@ -118,7 +122,9 @@ EOT } $this->json = new JsonFile($this->file); + $this->lock = Factory::getLockFile($this->file); $this->composerBackup = file_get_contents($this->json->getPath()); + $this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null; // check for writability by writing to the file as is_writable can not be trusted on network-mounts // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 @@ -325,9 +331,19 @@ EOT if ($this->newlyCreated) { $io->writeError("\n".'Installation failed, deleting '.$this->file.'.'); unlink($this->json->getPath()); + if (file_exists($this->lock)) { + unlink($this->lock); + } } else { - $io->writeError("\n".'Installation failed, reverting '.$this->file.' to its original content.'); + $msg = ' to its '; + if ($this->lockBackup) { + $msg = ' and '.$this->lock.' to their '; + } + $io->writeError("\n".'Installation failed, reverting '.$this->file.$msg.'original content.'); file_put_contents($this->json->getPath(), $this->composerBackup); + if ($this->lockBackup) { + file_put_contents($this->lock, $this->lockBackup); + } } if ($hardExit) { diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 02c786119..f50050604 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -223,6 +223,13 @@ class Factory return trim(getenv('COMPOSER')) ?: './composer.json'; } + public static function getLockFile($composerFile) + { + return "json" === pathinfo($composerFile, PATHINFO_EXTENSION) + ? substr($composerFile, 0, -4).'lock' + : $composerFile . '.lock'; + } + public static function createAdditionalStyles() { return array( @@ -388,9 +395,7 @@ class Factory // init locker if possible if ($fullLoad && isset($composerFile)) { - $lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION) - ? substr($composerFile, 0, -4).'lock' - : $composerFile . '.lock'; + $lockFile = self::getLockFile($composerFile); $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile)); $composer->setLocker($locker); From 9858718ef6dfcaa3d6a7a73654b62b82ac548ecd Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 15:32:02 +0200 Subject: [PATCH 316/321] Give a clearer error message explaining how to update a conflicting locked dependency --- .../DependencyResolver/PoolBuilder.php | 4 +- src/Composer/DependencyResolver/Problem.php | 12 +++- src/Composer/DependencyResolver/Rule.php | 5 ++ .../SolverProblemsException.php | 7 +++ ...update-allow-list-require-new-replace.test | 55 +++++++++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 305ad16b6..420e75786 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -268,8 +268,8 @@ class PoolBuilder } } - // if we're doing a partial update with deps and we're not loading an initial fixed package - // we also need to trigger an update for transitive deps which are being replaced + // 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 ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) { foreach ($package->getReplaces() as $link) { $replace = $link->getTarget(); diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index a26cc47de..13d0ffd4f 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -92,7 +92,6 @@ class Problem } $messages = array(); - foreach ($reasons as $rule) { $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); } @@ -100,6 +99,17 @@ class Problem return "\n - ".implode("\n - ", $messages); } + public function isCausedByLock() + { + foreach ($this->reasons as $sectionRules) { + foreach ($sectionRules as $rule) { + if ($rule->isCausedByLock()) { + return true; + } + } + } + } + /** * Store a reason descriptor but ignore duplicates * diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 58f6962cd..a0824b88a 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -123,6 +123,11 @@ abstract class Rule abstract public function isAssertion(); + public function isCausedByLock() + { + return $this->getReason() === self::RULE_FIXED && $this->reasonData['lockable']; + } + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) { $literals = $this->getLiterals(); diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index a47bd1c8c..cfaa110c6 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -36,12 +36,15 @@ class SolverProblemsException extends \RuntimeException $installedMap = $request->getPresentMap(true); $text = "\n"; $hasExtensionProblems = false; + $isCausedByLock = false; foreach ($this->problems as $i => $problem) { $text .= " Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; } + + $isCausedByLock |= $problem->isCausedByLock(); } if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) { @@ -52,6 +55,10 @@ class SolverProblemsException extends \RuntimeException $text .= $this->createExtensionHint(); } + if ($isCausedByLock && !$isDevExtraction) { + $text .= "\nUse the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions."; + } + return $text; } diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test new file mode 100644 index 000000000..45a9adb21 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test @@ -0,0 +1,55 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "current/dep": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--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 can not be installed as that would require removing current/dep 1.0.0. new/pkg replaces current/dep and can thus not 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 updates and removals for packages currently locked to specific versions. +--EXPECT-- From 88e3f24b916525831ef1995cc4e11e7d56c501b0 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 15:42:42 +0200 Subject: [PATCH 317/321] Fix description of test case --- .../installer/update-allow-list-require-new-replace.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test index 45a9adb21..c97f14b33 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test @@ -1,5 +1,5 @@ --TEST-- -Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +If a new requirement cannot be installed on a partial update due to replace, there should be a suggestion to use --with-all-dependencies --COMPOSER-- { "repositories": [ From 4fab2c7759ac07b6a18849b1fd86e93501ad5484 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 16:01:48 +0200 Subject: [PATCH 318/321] Error wording use "thus cannot" instead of "can thus not" --- src/Composer/DependencyResolver/Rule.php | 10 +++++----- .../Test/Fixtures/installer/provider-conflicts.test | 2 +- .../Test/Fixtures/installer/provider-conflicts3.test | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index a0824b88a..fe1afd12e 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -201,11 +201,11 @@ abstract class Rule $reason = null; if ($conflictingNames = array_values(array_intersect($replaces1, $replaces2))) { - $reason = 'They both replace '.(count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]).' and can thus not coexist.'; + $reason = 'They both replace '.(count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]).' and thus cannot coexist.'; } elseif (in_array($package1->getName(), $replaces2, true)) { - $reason = $package2->getName().' replaces '.$package1->getName().' and can thus not coexist with it.'; + $reason = $package2->getName().' replaces '.$package1->getName().' and thus cannot coexist with it.'; } elseif (in_array($package2->getName(), $replaces1, true)) { - $reason = $package1->getName().' replaces '.$package2->getName().' and can thus not coexist with it.'; + $reason = $package1->getName().' replaces '.$package2->getName().' and thus cannot coexist with it.'; } if ($reason) { @@ -216,7 +216,7 @@ abstract class Rule $package1 = $tmp; } if (!isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) { - return $package1->getPrettyString().' can not be installed as that would require removing '.$package2->getPrettyString().'. '.$reason; + return $package1->getPrettyString().' cannot be installed as that would require removing '.$package2->getPrettyString().'. '.$reason; } if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { @@ -261,7 +261,7 @@ abstract class Rule $replacedNames = count($replacedNames) > 1 ? '['.implode(', ', $replacedNames).']' : $replacedNames[0]; if ($replacer) { - return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. '.$replacer->getName().' replaces '.$replacedNames.' and can thus not coexist with it.'; + return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. '.$replacer->getName().' replaces '.$replacedNames.' and thus cannot coexist with it.'; } } diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test index 1c8bd5608..3a9700103 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test @@ -42,7 +42,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - __root__ is present at version 1.2.3 and cannot be modified by Composer - - provider/pkg 1.0.0 can not be installed as that would require removing __root__ 1.2.3. They both replace root-replaced/transitive-replaced and can thus not coexist. + - provider/pkg 1.0.0 cannot be installed as that would require removing __root__ 1.2.3. They both replace root-replaced/transitive-replaced and thus cannot coexist. - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test index 3a865f3bf..1c2ea0ceb 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -39,14 +39,14 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Conclusion: don't install regular/pkg 1.0.3, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - Conclusion: don't install regular/pkg 1.0.2, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - Conclusion: don't install regular/pkg 1.0.1, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3]. - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. From ce0c2d44cd46aef82e1115c83e5d0f483d76425e Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 16:20:54 +0200 Subject: [PATCH 319/321] Adjust forgotten error message in test to new language --- .../installer/update-allow-list-require-new-replace.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test index c97f14b33..8ddc44a5c 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test @@ -48,7 +48,7 @@ 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 can not be installed as that would require removing current/dep 1.0.0. new/pkg replaces current/dep and can thus not coexist with it. + - new/pkg 1.0.0 can not 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 updates and removals for packages currently locked to specific versions. From 8072ec9b7cf8e42732aca3bfe22cf2ecaf0570ed Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 2 Apr 2020 16:31:30 +0200 Subject: [PATCH 320/321] Number 2: Adjust forgotten error message in test to new language --- .../installer/update-allow-list-require-new-replace.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test index 8ddc44a5c..dc6e9aa5f 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test @@ -48,7 +48,7 @@ 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 can not be installed as that would require removing current/dep 1.0.0. new/pkg replaces current/dep and thus cannot coexist with it. + - 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 updates and removals for packages currently locked to specific versions. From 1c73f078f7ec2e8048e98970ba1e617cc2d30b2c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 6 Apr 2020 21:17:03 +0200 Subject: [PATCH 321/321] Remove repository field from getProviders result --- src/Composer/Repository/ArrayRepository.php | 1 - src/Composer/Repository/ComposerRepository.php | 1 - src/Composer/Repository/RepositoryInterface.php | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index 69ea0d080..37a241cbc 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -221,7 +221,6 @@ class ArrayRepository implements RepositoryInterface 'name' => $candidate->getName(), 'description' => $candidate->getDescription(), 'type' => $candidate->getType(), - 'repository' => $candidate->getSourceUrl() ?: '', ); continue 2; } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index dc455dc8d..e705e6852 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -444,7 +444,6 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito 'name' => $candidate['name'], 'description' => isset($candidate['description']) ? $candidate['description'] : '', 'type' => isset($candidate['type']) ? $candidate['type'] : '', - 'repository' => '', ); } } diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 75813c085..98b616fa0 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -91,7 +91,7 @@ interface RepositoryInterface extends \Countable * * @param string $packageName package name which must be provided * - * @return array[] an array with the provider name as key and value of array('name' => '...', 'description' => '...', 'type' => '...', 'repository' => '...url to source repo if available...') + * @return array[] an array with the provider name as key and value of array('name' => '...', 'description' => '...', 'type' => '...') */ public function getProviders($packageName);