From b6c9d3412506dbfefb727649af50c8a4fca08b84 Mon Sep 17 00:00:00 2001 From: Jochen Roth Date: Thu, 25 Feb 2021 10:00:36 +0100 Subject: [PATCH 1/8] Add install-path and type to installedVersions.php and installed.php, add method to get installed packages by type Issue https://github.com/composer/composer/issues/9648 --- src/Composer/InstalledVersions.php | 31 +++++++++- .../Repository/FilesystemRepository.php | 57 +++++++++++++++++-- tests/Composer/Test/InstalledVersionsTest.php | 21 +++++++ .../Test/Mock/InstallationManagerMock.php | 5 ++ .../Repository/FilesystemRepositoryTest.php | 5 ++ .../Test/Repository/Fixtures/installed.php | 18 ++++++ 6 files changed, 131 insertions(+), 6 deletions(-) diff --git a/src/Composer/InstalledVersions.php b/src/Composer/InstalledVersions.php index e6df5a5a2..5126c5f8b 100644 --- a/src/Composer/InstalledVersions.php +++ b/src/Composer/InstalledVersions.php @@ -24,10 +24,15 @@ use Composer\Semver\VersionParser; */ class InstalledVersions { - private static $installed; + private static $installed = array(); private static $canGetVendors; private static $installedByVendor = array(); + /** + * Initialize $installed array + */ + public static function initializeInstalled() {} + /** * Returns a list of all package names which are present, either by being installed, replaced or provided * @@ -48,6 +53,28 @@ class InstalledVersions return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + /** * Checks whether the given package is installed * @@ -274,3 +301,5 @@ class InstalledVersions return $installed; } } + +InstalledVersions::initializeInstalled(); diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index fefc52dfe..99af09be7 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -14,6 +14,7 @@ namespace Composer\Repository; use Composer\Json\JsonFile; use Composer\Package\Loader\ArrayLoader; +use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Package\AliasPackage; use Composer\Package\Dumper\ArrayDumper; @@ -102,11 +103,15 @@ class FilesystemRepository extends WritableArrayRepository $dumper = new ArrayDumper(); $fs = new Filesystem(); $repoDir = dirname($fs->normalizePath($this->file->getPath())); + $installPaths = array(); foreach ($this->getCanonicalPackages() as $package) { $pkgArray = $dumper->dump($package); $path = $installationManager->getInstallPath($package); - $pkgArray['install-path'] = ('' !== $path && null !== $path) ? $fs->findShortestPath($repoDir, $fs->isAbsolutePath($path) ? $path : getcwd() . '/' . $path, true) : null; + $installPath = ('' !== $path && null !== $path) ? $fs->findShortestPath($repoDir, $fs->isAbsolutePath($path) ? $path : getcwd() . '/' . $path, true) : null; + $installPaths[$package->getName()] = $installPath; + + $pkgArray['install-path'] = $installPath; $data['packages'][] = $pkgArray; // only write to the files the names which are really installed, as we receive the full list @@ -124,24 +129,56 @@ class FilesystemRepository extends WritableArrayRepository $this->file->write($data); if ($this->dumpVersions) { - $versions = $this->generateInstalledVersions($installationManager, $devMode); + $versions = $this->generateInstalledVersions($installationManager, $installPaths, $devMode, $repoDir); - $fs->filePutContentsIfModified($repoDir.'/installed.php', 'filePutContentsIfModified($repoDir.'/installed.php', 'dumpVersion($versions) . ';'."\n"); $installedVersionsClass = file_get_contents(__DIR__.'/../InstalledVersions.php'); // while not strictly needed since https://github.com/composer/composer/pull/9635 - we keep this for BC // and overall broader compatibility with people that may not use Composer's ClassLoader. They can // simply include InstalledVersions.php manually and have it working in a basic way. - $installedVersionsClass = str_replace('private static $installed;', 'private static $installed = '.var_export($versions, true).';', $installedVersionsClass); + $installedVersionsClass = str_replace('public static function initializeInstalled() {}', 'public static function initializeInstalled() {' . PHP_EOL . 'self::$installed = ' . $this->dumpVersion($versions) . ';' . PHP_EOL . '}', $installedVersionsClass); $fs->filePutContentsIfModified($repoDir.'/InstalledVersions.php', $installedVersionsClass); \Composer\InstalledVersions::reload($versions); } } + private function dumpVersion(array $array = array(), $level = 0) + { + $lines = "array(\n"; + $level++; + + foreach ($array as $key => $value) { + $lines .= str_repeat(' ', $level); + $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => '; + + if (is_array($value)) { + if (!empty($value)) { + $lines .= self::dumpVersion($value, $level); + } else { + $lines .= "array(),\n"; + } + } elseif (is_null($value)) { + $lines .= 'null'; + $lines .= ",\n"; + } elseif (is_bool($value)) { + $lines .= $value ? 'true' : 'false'; + $lines .= ",\n"; + } else { + $stringContent = str_replace(array('\\', '\''), array('\\\\', '\\\''), $value); + $folder = $key === 'install_path' ? '__DIR__ . DIRECTORY_SEPARATOR . ' : ''; + $lines .= $folder . "'" . $stringContent . "',\n"; + } + } + + $lines .= str_repeat(' ', $level - 1) . ')' . ($level - 1 == 0 ? '' : ",\n"); + return $lines; + } + /** * @return ?array */ - private function generateInstalledVersions(InstallationManager $installationManager, $devMode) + private function generateInstalledVersions(InstallationManager $installationManager, $installPaths, $devMode, $repoDir) { if (!$this->dumpVersions) { return null; @@ -170,9 +207,19 @@ class FilesystemRepository extends WritableArrayRepository $reference = ($package->getSourceReference() ?: $package->getDistReference()) ?: null; } + if($package instanceof RootPackageInterface) { + $fs = new Filesystem(); + $to = getcwd(); + $installPath = $fs->findShortestPath($repoDir, $to, true); + } else { + $installPath = $installPaths[$package->getName()]; + } + $versions['versions'][$package->getName()] = array( 'pretty_version' => $package->getPrettyVersion(), 'version' => $package->getVersion(), + 'type' => $package->getType(), + 'install_path' => $installPath, 'aliases' => array(), 'reference' => $reference, 'dev-requirement' => isset($devPackages[$package->getName()]), diff --git a/tests/Composer/Test/InstalledVersionsTest.php b/tests/Composer/Test/InstalledVersionsTest.php index 7896548e2..bfd7feb78 100644 --- a/tests/Composer/Test/InstalledVersionsTest.php +++ b/tests/Composer/Test/InstalledVersionsTest.php @@ -17,6 +17,8 @@ use Composer\Semver\VersionParser; class InstalledVersionsTest extends TestCase { + private $root; + public static function setUpBeforeClass() { // disable multiple-ClassLoader-based checks of InstalledVersions by making it seem like no @@ -33,6 +35,9 @@ class InstalledVersionsTest extends TestCase public function setUp() { + $this->root = $this->getUniqueTmpDirectory(); + + $dir = $this->root; InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed.php'); } @@ -187,6 +192,8 @@ class InstalledVersionsTest extends TestCase $this->assertSame(array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', + 'type' => 'library', + 'install_path' => $this->root . DIRECTORY_SEPARATOR . './', 'aliases' => array( '1.10.x-dev', ), @@ -198,6 +205,7 @@ class InstalledVersionsTest extends TestCase public function testGetRawData() { + $dir = $this->root; $this->assertSame(require __DIR__.'/Repository/Fixtures/installed.php', InstalledVersions::getRawData()); } @@ -222,4 +230,17 @@ class InstalledVersionsTest extends TestCase array(null, 'c/c'), ); } + + public function testGetInstalledPackagesByType() + { + $names = array( + '__root__', + 'a/provider', + 'a/provider2', + 'b/replacer', + 'c/c', + ); + + $this->assertSame($names, \Composer\InstalledVersions::getInstalledPackagesByType('library')); + } } diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index 4642a4800..7898250cc 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -119,4 +119,9 @@ class InstallationManagerMock extends InstallationManager { // noop } + + public function getInstalledPackagesByType() + { + return $this->installed; + } } diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index 72e7adfa3..41d6ff0c1 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -18,6 +18,8 @@ use Composer\Json\JsonFile; class FilesystemRepositoryTest extends TestCase { + private $root; + public function testRepositoryRead() { $json = $this->createJsonFileMock(); @@ -121,6 +123,9 @@ class FilesystemRepositoryTest extends TestCase public function testRepositoryWritesInstalledPhp() { $dir = $this->getUniqueTmpDirectory(); + $this->root = $dir; + chdir($dir); + $json = new JsonFile($dir.'/installed.json'); $rootPackage = $this->getPackage('__root__', 'dev-master', 'Composer\Package\RootPackage'); diff --git a/tests/Composer/Test/Repository/Fixtures/installed.php b/tests/Composer/Test/Repository/Fixtures/installed.php index 8c6c148ae..0f6283c7e 100644 --- a/tests/Composer/Test/Repository/Fixtures/installed.php +++ b/tests/Composer/Test/Repository/Fixtures/installed.php @@ -14,6 +14,9 @@ return array( 'root' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . DIRECTORY_SEPARATOR . './', 'aliases' => array( '1.10.x-dev', ), @@ -25,6 +28,9 @@ return array( '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . DIRECTORY_SEPARATOR . './', 'aliases' => array( '1.10.x-dev', ), @@ -34,6 +40,9 @@ return array( 'a/provider' => array( 'pretty_version' => '1.1', 'version' => '1.1.0.0', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . DIRECTORY_SEPARATOR . '/foo/bar/vendor/woop/woop', 'aliases' => array(), 'reference' => 'distref-as-no-source', 'dev-requirement' => false, @@ -41,6 +50,9 @@ return array( 'a/provider2' => array( 'pretty_version' => '1.2', 'version' => '1.2.0.0', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . DIRECTORY_SEPARATOR . '/foo/bar/vendor/woop/woop', 'aliases' => array( '1.4', ), @@ -50,6 +62,9 @@ return array( 'b/replacer' => array( 'pretty_version' => '2.2', 'version' => '2.2.0.0', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . DIRECTORY_SEPARATOR . '/foo/bar/vendor/woop/woop', 'aliases' => array(), 'reference' => null, 'dev-requirement' => false, @@ -57,6 +72,9 @@ return array( 'c/c' => array( 'pretty_version' => '3.0', 'version' => '3.0.0.0', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . DIRECTORY_SEPARATOR . '/foo/bar/vendor/woop/woop', 'aliases' => array(), 'reference' => null, 'dev-requirement' => true, From 8335b49cf0b62d3ea205d3797e1720058d9c2ab4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 21 May 2021 13:17:47 +0200 Subject: [PATCH 2/8] Rename dev-requirement to dev_requirement for consistency --- src/Composer/InstalledVersions.php | 8 ++++---- src/Composer/Repository/FilesystemRepository.php | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Composer/InstalledVersions.php b/src/Composer/InstalledVersions.php index 5126c5f8b..8384c86f8 100644 --- a/src/Composer/InstalledVersions.php +++ b/src/Composer/InstalledVersions.php @@ -88,7 +88,7 @@ class InstalledVersions { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { - return $includeDevRequirements || empty($installed['versions'][$packageName]['dev-requirement']); + return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); } } @@ -230,7 +230,7 @@ class InstalledVersions * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] - * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool}, versions: array} + * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool}, versions: array} */ public static function getRawData() { @@ -266,7 +266,7 @@ class InstalledVersions * @param array[] $data A vendor/composer/installed.php data set * @return void * - * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool}, versions: array} $data + * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool}, versions: array} $data */ public static function reload($data) { @@ -276,7 +276,7 @@ class InstalledVersions /** * @return array[] - * @psalm-return list}> + * @psalm-return list}> */ private static function getInstalled() { diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index 99af09be7..bf2440bc8 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -222,11 +222,11 @@ class FilesystemRepository extends WritableArrayRepository 'install_path' => $installPath, 'aliases' => array(), 'reference' => $reference, - 'dev-requirement' => isset($devPackages[$package->getName()]), + 'dev_requirement' => isset($devPackages[$package->getName()]), ); if ($package instanceof RootPackageInterface) { $versions['root'] = $versions['versions'][$package->getName()]; - unset($versions['root']['dev-requirement']); + unset($versions['root']['dev_requirement']); $versions['root']['name'] = $package->getName(); $versions['root']['dev'] = $devMode; } @@ -240,10 +240,10 @@ class FilesystemRepository extends WritableArrayRepository if (PlatformRepository::isPlatformPackage($replace->getTarget())) { continue; } - if (!isset($versions['versions'][$replace->getTarget()]['dev-requirement'])) { - $versions['versions'][$replace->getTarget()]['dev-requirement'] = $isDevPackage; + if (!isset($versions['versions'][$replace->getTarget()]['dev_requirement'])) { + $versions['versions'][$replace->getTarget()]['dev_requirement'] = $isDevPackage; } elseif (!$isDevPackage) { - $versions['versions'][$replace->getTarget()]['dev-requirement'] = false; + $versions['versions'][$replace->getTarget()]['dev_requirement'] = false; } $replaced = $replace->getPrettyConstraint(); if ($replaced === 'self.version') { @@ -258,10 +258,10 @@ class FilesystemRepository extends WritableArrayRepository if (PlatformRepository::isPlatformPackage($provide->getTarget())) { continue; } - if (!isset($versions['versions'][$provide->getTarget()]['dev-requirement'])) { - $versions['versions'][$provide->getTarget()]['dev-requirement'] = $isDevPackage; + if (!isset($versions['versions'][$provide->getTarget()]['dev_requirement'])) { + $versions['versions'][$provide->getTarget()]['dev_requirement'] = $isDevPackage; } elseif (!$isDevPackage) { - $versions['versions'][$provide->getTarget()]['dev-requirement'] = false; + $versions['versions'][$provide->getTarget()]['dev_requirement'] = false; } $provided = $provide->getPrettyConstraint(); if ($provided === 'self.version') { From 5a69a1e483a4f344c03539153856172b65f8846f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 21 May 2021 13:23:16 +0200 Subject: [PATCH 3/8] Add InstalledVersions::getInstallPath($package) getter, and update phpdoc types --- src/Composer/InstalledVersions.php | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Composer/InstalledVersions.php b/src/Composer/InstalledVersions.php index 8384c86f8..81f4a6227 100644 --- a/src/Composer/InstalledVersions.php +++ b/src/Composer/InstalledVersions.php @@ -214,9 +214,26 @@ class InstalledVersions throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + /** * @return array - * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool} + * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string} */ public static function getRootPackage() { @@ -230,7 +247,7 @@ class InstalledVersions * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] - * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool}, versions: array} + * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array} */ public static function getRawData() { @@ -266,7 +283,7 @@ class InstalledVersions * @param array[] $data A vendor/composer/installed.php data set * @return void * - * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool}, versions: array} $data + * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array} $data */ public static function reload($data) { @@ -276,7 +293,7 @@ class InstalledVersions /** * @return array[] - * @psalm-return list}> + * @psalm-return list}> */ private static function getInstalled() { From 518b44a810adcc3e15f2824c59b1e134fadd727e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 21 May 2021 13:25:31 +0200 Subject: [PATCH 4/8] Clean up dump code to avoid reimplementing var export, and remove DIRECTORY_SEPARATOR --- .../Repository/FilesystemRepository.php | 26 ++++++++--------- tests/Composer/Test/InstalledVersionsTest.php | 9 +++++- .../Test/Repository/Fixtures/installed.php | 28 +++++++++---------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index bf2440bc8..71a8fa6fe 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -130,20 +130,21 @@ class FilesystemRepository extends WritableArrayRepository if ($this->dumpVersions) { $versions = $this->generateInstalledVersions($installationManager, $installPaths, $devMode, $repoDir); + $versionsCode = $this->dumpToPhpCode($versions); - $fs->filePutContentsIfModified($repoDir.'/installed.php', 'dumpVersion($versions) . ';'."\n"); + $fs->filePutContentsIfModified($repoDir.'/installed.php', 'dumpVersion($versions) . ';' . PHP_EOL . '}', $installedVersionsClass); + $installedVersionsClass = str_replace('public static function initializeInstalled() {}', 'public static function initializeInstalled() {' . PHP_EOL . 'self::$installed = ' . $versionsCode . ';' . PHP_EOL . '}', $installedVersionsClass); $fs->filePutContentsIfModified($repoDir.'/InstalledVersions.php', $installedVersionsClass); \Composer\InstalledVersions::reload($versions); } } - private function dumpVersion(array $array = array(), $level = 0) + private function dumpToPhpCode(array $array = array(), $level = 0) { $lines = "array(\n"; $level++; @@ -154,31 +155,26 @@ class FilesystemRepository extends WritableArrayRepository if (is_array($value)) { if (!empty($value)) { - $lines .= self::dumpVersion($value, $level); + $lines .= $this->dumpToPhpCode($value, $level); } else { $lines .= "array(),\n"; } - } elseif (is_null($value)) { - $lines .= 'null'; - $lines .= ",\n"; - } elseif (is_bool($value)) { - $lines .= $value ? 'true' : 'false'; - $lines .= ",\n"; + } elseif ($key === 'install_path') { + $lines .= "__DIR__ . " . var_export('/' . $value, true) . ",\n"; } else { - $stringContent = str_replace(array('\\', '\''), array('\\\\', '\\\''), $value); - $folder = $key === 'install_path' ? '__DIR__ . DIRECTORY_SEPARATOR . ' : ''; - $lines .= $folder . "'" . $stringContent . "',\n"; + $lines .= var_export($value, true) . ",\n"; } } $lines .= str_repeat(' ', $level - 1) . ')' . ($level - 1 == 0 ? '' : ",\n"); + return $lines; } /** * @return ?array */ - private function generateInstalledVersions(InstallationManager $installationManager, $installPaths, $devMode, $repoDir) + private function generateInstalledVersions(InstallationManager $installationManager, array $installPaths, $devMode, $repoDir) { if (!$this->dumpVersions) { return null; @@ -207,7 +203,7 @@ class FilesystemRepository extends WritableArrayRepository $reference = ($package->getSourceReference() ?: $package->getDistReference()) ?: null; } - if($package instanceof RootPackageInterface) { + if ($package instanceof RootPackageInterface) { $fs = new Filesystem(); $to = getcwd(); $installPath = $fs->findShortestPath($repoDir, $to, true); diff --git a/tests/Composer/Test/InstalledVersionsTest.php b/tests/Composer/Test/InstalledVersionsTest.php index bfd7feb78..80947fc02 100644 --- a/tests/Composer/Test/InstalledVersionsTest.php +++ b/tests/Composer/Test/InstalledVersionsTest.php @@ -193,7 +193,7 @@ class InstalledVersionsTest extends TestCase 'pretty_version' => 'dev-master', 'version' => 'dev-master', 'type' => 'library', - 'install_path' => $this->root . DIRECTORY_SEPARATOR . './', + 'install_path' => $this->root . '/./', 'aliases' => array( '1.10.x-dev', ), @@ -243,4 +243,11 @@ class InstalledVersionsTest extends TestCase $this->assertSame($names, \Composer\InstalledVersions::getInstalledPackagesByType('library')); } + + public function testGetInstallPath() + { + $this->assertSame($this->root . '/./', \Composer\InstalledVersions::getInstallPath('__root__')); + $this->assertSame($this->root . '/foo/bar/vendor/woop/woop', \Composer\InstalledVersions::getInstallPath('c/c')); + $this->assertNull(\Composer\InstalledVersions::getInstallPath('foo/impl')); + } } diff --git a/tests/Composer/Test/Repository/Fixtures/installed.php b/tests/Composer/Test/Repository/Fixtures/installed.php index 0f6283c7e..a66c75e50 100644 --- a/tests/Composer/Test/Repository/Fixtures/installed.php +++ b/tests/Composer/Test/Repository/Fixtures/installed.php @@ -16,7 +16,7 @@ return array( 'version' => 'dev-master', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . DIRECTORY_SEPARATOR . './', + 'install_path' => $dir . '/./', 'aliases' => array( '1.10.x-dev', ), @@ -30,57 +30,57 @@ return array( 'version' => 'dev-master', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . DIRECTORY_SEPARATOR . './', + 'install_path' => $dir . '/./', 'aliases' => array( '1.10.x-dev', ), 'reference' => 'sourceref-by-default', - 'dev-requirement' => false, + 'dev_requirement' => false, ), 'a/provider' => array( 'pretty_version' => '1.1', 'version' => '1.1.0.0', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . DIRECTORY_SEPARATOR . '/foo/bar/vendor/woop/woop', + 'install_path' => $dir . '/foo/bar/vendor/woop/woop', 'aliases' => array(), 'reference' => 'distref-as-no-source', - 'dev-requirement' => false, + 'dev_requirement' => false, ), 'a/provider2' => array( 'pretty_version' => '1.2', 'version' => '1.2.0.0', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . DIRECTORY_SEPARATOR . '/foo/bar/vendor/woop/woop', + 'install_path' => $dir . '/foo/bar/vendor/woop/woop', 'aliases' => array( '1.4', ), 'reference' => 'distref-as-installed-from-dist', - 'dev-requirement' => false, + 'dev_requirement' => false, ), 'b/replacer' => array( 'pretty_version' => '2.2', 'version' => '2.2.0.0', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . DIRECTORY_SEPARATOR . '/foo/bar/vendor/woop/woop', + 'install_path' => $dir . '/foo/bar/vendor/woop/woop', 'aliases' => array(), 'reference' => null, - 'dev-requirement' => false, + 'dev_requirement' => false, ), 'c/c' => array( 'pretty_version' => '3.0', 'version' => '3.0.0.0', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . DIRECTORY_SEPARATOR . '/foo/bar/vendor/woop/woop', + 'install_path' => $dir . '/foo/bar/vendor/woop/woop', 'aliases' => array(), 'reference' => null, - 'dev-requirement' => true, + 'dev_requirement' => true, ), 'foo/impl' => array( - 'dev-requirement' => false, + 'dev_requirement' => false, 'provided' => array( '^1.1', '1.2', @@ -89,7 +89,7 @@ return array( ), ), 'foo/impl2' => array( - 'dev-requirement' => false, + 'dev_requirement' => false, 'provided' => array( '2.0', ), @@ -98,7 +98,7 @@ return array( ), ), 'foo/replaced' => array( - 'dev-requirement' => false, + 'dev_requirement' => false, 'replaced' => array( '^3.0', ), From 3fe4f84a76966f5c522e7d87fd676000d79347ea Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 21 May 2021 13:54:18 +0200 Subject: [PATCH 5/8] Fix handling of metapackages with null paths, and handling of paths which do not have a shortest-path and require an absolute path to be addressed --- .../Repository/FilesystemRepository.php | 9 +++++-- tests/Composer/Test/InstalledVersionsTest.php | 6 +++-- .../Repository/FilesystemRepositoryTest.php | 25 ++++++++++++++++++- .../Test/Repository/Fixtures/installed.php | 18 +++++++++---- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index 71a8fa6fe..6b44d97d9 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -159,8 +159,13 @@ class FilesystemRepository extends WritableArrayRepository } else { $lines .= "array(),\n"; } - } elseif ($key === 'install_path') { - $lines .= "__DIR__ . " . var_export('/' . $value, true) . ",\n"; + } elseif ($key === 'install_path' && is_string($value)) { + $fs = new Filesystem(); + if ($fs->isAbsolutePath($value)) { + $lines .= var_export($value, true) . ",\n"; + } else { + $lines .= "__DIR__ . " . var_export('/' . $value, true) . ",\n"; + } } else { $lines .= var_export($value, true) . ",\n"; } diff --git a/tests/Composer/Test/InstalledVersionsTest.php b/tests/Composer/Test/InstalledVersionsTest.php index 80947fc02..d48cdf09a 100644 --- a/tests/Composer/Test/InstalledVersionsTest.php +++ b/tests/Composer/Test/InstalledVersionsTest.php @@ -52,6 +52,7 @@ class InstalledVersionsTest extends TestCase 'foo/impl', 'foo/impl2', 'foo/replaced', + 'meta/package', ); $this->assertSame($names, InstalledVersions::getInstalledPackages()); } @@ -74,6 +75,7 @@ class InstalledVersionsTest extends TestCase array(true, '__root__'), array(true, 'b/replacer'), array(false, 'not/there'), + array(true, 'meta/package'), ); } @@ -246,8 +248,8 @@ class InstalledVersionsTest extends TestCase public function testGetInstallPath() { - $this->assertSame($this->root . '/./', \Composer\InstalledVersions::getInstallPath('__root__')); - $this->assertSame($this->root . '/foo/bar/vendor/woop/woop', \Composer\InstalledVersions::getInstallPath('c/c')); + $this->assertSame(realpath($this->root), realpath(\Composer\InstalledVersions::getInstallPath('__root__'))); + $this->assertSame('/foo/bar/vendor/c/c', \Composer\InstalledVersions::getInstallPath('c/c')); $this->assertNull(\Composer\InstalledVersions::getInstallPath('foo/impl')); } } diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index 41d6ff0c1..da754661b 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Repository; +use Composer\Package\RootPackageInterface; use Composer\Repository\FilesystemRepository; use Composer\Test\TestCase; use Composer\Json\JsonFile; @@ -157,12 +158,34 @@ class FilesystemRepositoryTest extends TestCase $pkg = $this->getPackage('c/c', '3.0'); $repository->addPackage($pkg); + $pkg = $this->getPackage('meta/package', '3.0'); + $pkg->setType('metapackage'); + $repository->addPackage($pkg); + $im = $this->getMockBuilder('Composer\Installer\InstallationManager') ->disableOriginalConstructor() ->getMock(); $im->expects($this->any()) ->method('getInstallPath') - ->will($this->returnValue('/foo/bar/vendor/woop/woop')); + ->will($this->returnCallback(function ($package) use ($dir) { + // check for empty paths handling + if ($package->getType() === 'metapackage') { + return ''; + } + + if ($package->getName() === 'c/c') { + // check for absolute paths + return '/foo/bar/vendor/c/c'; + } + + // check for cwd + if ($package instanceof RootPackageInterface) { + return $dir; + } + + // check for relative paths + return 'vendor/'.$package->getName(); + })); $repository->write(true, $im); $this->assertSame(require __DIR__.'/Fixtures/installed.php', require $dir.'/installed.php'); diff --git a/tests/Composer/Test/Repository/Fixtures/installed.php b/tests/Composer/Test/Repository/Fixtures/installed.php index a66c75e50..fb9870def 100644 --- a/tests/Composer/Test/Repository/Fixtures/installed.php +++ b/tests/Composer/Test/Repository/Fixtures/installed.php @@ -42,7 +42,7 @@ return array( 'version' => '1.1.0.0', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . '/foo/bar/vendor/woop/woop', + 'install_path' => $dir . '/vendor/a/provider', 'aliases' => array(), 'reference' => 'distref-as-no-source', 'dev_requirement' => false, @@ -52,7 +52,7 @@ return array( 'version' => '1.2.0.0', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . '/foo/bar/vendor/woop/woop', + 'install_path' => $dir . '/vendor/a/provider2', 'aliases' => array( '1.4', ), @@ -64,7 +64,7 @@ return array( 'version' => '2.2.0.0', 'type' => 'library', // @phpstan-ignore-next-line - 'install_path' => $dir . '/foo/bar/vendor/woop/woop', + 'install_path' => $dir . '/vendor/b/replacer', 'aliases' => array(), 'reference' => null, 'dev_requirement' => false, @@ -73,8 +73,7 @@ return array( 'pretty_version' => '3.0', 'version' => '3.0.0.0', 'type' => 'library', - // @phpstan-ignore-next-line - 'install_path' => $dir . '/foo/bar/vendor/woop/woop', + 'install_path' => '/foo/bar/vendor/c/c', 'aliases' => array(), 'reference' => null, 'dev_requirement' => true, @@ -103,5 +102,14 @@ return array( '^3.0', ), ), + 'meta/package' => array( + 'pretty_version' => '3.0', + 'version' => '3.0.0.0', + 'type' => 'metapackage', + 'install_path' => null, + 'aliases' => array(), + 'reference' => null, + 'dev_requirement' => false, + ) ), ); From 82cffa17d306fe6b8a50166613f31000a9cf9ffa Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 21 May 2021 14:11:25 +0200 Subject: [PATCH 6/8] Undo addition of initializeInstalled and remove inline copy of the data from the class --- src/Composer/InstalledVersions.php | 21 +++++++++++-------- .../Repository/FilesystemRepository.php | 7 +------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Composer/InstalledVersions.php b/src/Composer/InstalledVersions.php index 81f4a6227..2c6c06f70 100644 --- a/src/Composer/InstalledVersions.php +++ b/src/Composer/InstalledVersions.php @@ -24,15 +24,10 @@ use Composer\Semver\VersionParser; */ class InstalledVersions { - private static $installed = array(); + private static $installed; private static $canGetVendors; private static $installedByVendor = array(); - /** - * Initialize $installed array - */ - public static function initializeInstalled() {} - /** * Returns a list of all package names which are present, either by being installed, replaced or provided * @@ -253,6 +248,10 @@ class InstalledVersions { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + if (null === self::$installed) { + self::$installed = include __DIR__ . '/installed.php'; + } + return self::$installed; } @@ -260,7 +259,7 @@ class InstalledVersions * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] - * @psalm-return list}> + * @psalm-return list}> */ public static function getAllRawData() { @@ -309,14 +308,18 @@ class InstalledVersions $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } } } } + if (null === self::$installed) { + self::$installed = require __DIR__ . '/installed.php'; + } $installed[] = self::$installed; return $installed; } } - -InstalledVersions::initializeInstalled(); diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index 6b44d97d9..a80777033 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -130,14 +130,9 @@ class FilesystemRepository extends WritableArrayRepository if ($this->dumpVersions) { $versions = $this->generateInstalledVersions($installationManager, $installPaths, $devMode, $repoDir); - $versionsCode = $this->dumpToPhpCode($versions); - $fs->filePutContentsIfModified($repoDir.'/installed.php', 'filePutContentsIfModified($repoDir.'/installed.php', 'dumpToPhpCode($versions) . ';'."\n"); $installedVersionsClass = file_get_contents(__DIR__.'/../InstalledVersions.php'); - // while not strictly needed since https://github.com/composer/composer/pull/9635 - we keep this for BC - // and overall broader compatibility with people that may not use Composer's ClassLoader. They can - // simply include InstalledVersions.php manually and have it working in a basic way. - $installedVersionsClass = str_replace('public static function initializeInstalled() {}', 'public static function initializeInstalled() {' . PHP_EOL . 'self::$installed = ' . $versionsCode . ';' . PHP_EOL . '}', $installedVersionsClass); $fs->filePutContentsIfModified($repoDir.'/InstalledVersions.php', $installedVersionsClass); \Composer\InstalledVersions::reload($versions); From 284ec95712d397d8d46fcfffc245ad87f3abebe9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 21 May 2021 14:30:34 +0200 Subject: [PATCH 7/8] Document new methods and update the composer-runtime-api version to 2.1 --- doc/07-runtime.md | 37 +++++++++++++++++++++++++++++++++++++ src/Composer/Composer.php | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/doc/07-runtime.md b/doc/07-runtime.md index 933897732..17aa6bce6 100644 --- a/doc/07-runtime.md +++ b/doc/07-runtime.md @@ -27,6 +27,14 @@ The main use cases for this class are the following: \Composer\InstalledVersions::isInstalled('psr/log-implementation'); // returns bool ``` +As of Composer 2.1, you may also check if something was installed via require-dev or not by +passing false as second argument: + +```php +\Composer\InstalledVersions::isInstalled('vendor/package'); // returns true assuming this package is installed +\Composer\InstalledVersions::isInstalled('vendor/package', false); // returns true if vendor/package is in require, false if in require-dev +``` + Note that this can not be used to check whether platform packages are installed. ### Knowing whether package X is installed in version Y @@ -89,6 +97,35 @@ possible for safety. A few other methods are available for more complex usages, please refer to the source/docblocks of [the class itself](https://github.com/composer/composer/blob/master/src/Composer/InstalledVersions.php). +### Knowing the path in which a package is installed + +The `getInstallPath` method to retrieve a package's absolute install path. + +```php +// returns an absolute path to the package installation location if vendor/package is installed, +// or null if it is provided/replaced, or the package is a metapackage +// or throws OutOfBoundsException if the package is not installed at all +\Composer\InstalledVersions::getInstallPath('vendor/package'); +``` + +> Available as of Composer 2.1 (i.e. `composer-runtime-api ^2.1`) + +### Knowing which packages of a given type are installed + +The `getInstalledPackagesByType` method accepts a package type (e.g. foo-plugin) and lists +the packages of that type which are installed. You can then use the methods above to retrieve +more information about each package if needed. + +This method should alleviate the need for custom installers placing plugins in a specific path +instead of leaving them in the vendor dir. You can then find plugins to initialize at runtime +via InstalledVersions, including their paths via getInstallPath if needed. + +```php +\Composer\InstalledVersions::getInstalledPackagesByType('foo-plugin'); +``` + +> Available as of Composer 2.1 (i.e. `composer-runtime-api ^2.1`) + ## Platform check composer-runtime-api 2.0 introduced a new `vendor/composer/platform_check.php` file, which diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 725c47a8a..80a3ea0c5 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -65,7 +65,7 @@ class Composer * * @var string */ - const RUNTIME_API_VERSION = '2.0.0'; + const RUNTIME_API_VERSION = '2.1.0'; public static function getVersion() { From 019d051e9eae84894869d388b1347be47aae648e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 21 May 2021 14:50:30 +0200 Subject: [PATCH 8/8] Fix type --- src/Composer/InstalledVersions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/InstalledVersions.php b/src/Composer/InstalledVersions.php index 2c6c06f70..2c3bcd1bf 100644 --- a/src/Composer/InstalledVersions.php +++ b/src/Composer/InstalledVersions.php @@ -259,7 +259,7 @@ class InstalledVersions * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] - * @psalm-return list}> + * @psalm-return list}> */ public static function getAllRawData() {