1
0
Fork 0

Merge pull request #9699 from ochorocho/improve-installed-versions-9648

Add install-path and type to installedVersions.php and installed.php,…
pull/9912/head
Jordi Boggiano 2021-05-24 10:29:40 +02:00 committed by GitHub
commit da3d5e3143
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 250 additions and 32 deletions

View File

@ -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

View File

@ -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()
{

View File

@ -48,6 +48,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<string>
*/
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
*
@ -61,7 +83,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']);
}
}
@ -187,9 +209,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()
{
@ -203,12 +242,16 @@ 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<string, array{dev-requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[]}>}
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>}
*/
public static function getRawData()
{
@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;
}
@ -216,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<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[]}, versions: array<string, array{pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[]}>}>
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>}>
*/
public static function getAllRawData()
{
@ -239,7 +282,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<string, array{dev-requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[]}>} $data
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>} $data
*/
public static function reload($data)
{
@ -249,7 +292,7 @@ class InstalledVersions
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool}, versions: array<string, array{dev-requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[]}>}>
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string}>}>
*/
private static function getInstalled()
{
@ -265,10 +308,16 @@ 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;

View File

@ -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,52 @@ 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', '<?php return '.var_export($versions, true).';'."\n");
$fs->filePutContentsIfModified($repoDir.'/installed.php', '<?php return ' . $this->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('private static $installed;', 'private static $installed = '.var_export($versions, true).';', $installedVersionsClass);
$fs->filePutContentsIfModified($repoDir.'/InstalledVersions.php', $installedVersionsClass);
\Composer\InstalledVersions::reload($versions);
}
}
private function dumpToPhpCode(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 .= $this->dumpToPhpCode($value, $level);
} else {
$lines .= "array(),\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";
}
}
$lines .= str_repeat(' ', $level - 1) . ')' . ($level - 1 == 0 ? '' : ",\n");
return $lines;
}
/**
* @return ?array
*/
private function generateInstalledVersions(InstallationManager $installationManager, $devMode)
private function generateInstalledVersions(InstallationManager $installationManager, array $installPaths, $devMode, $repoDir)
{
if (!$this->dumpVersions) {
return null;
@ -170,16 +203,26 @@ 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()]),
'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;
}
@ -193,10 +236,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') {
@ -211,10 +254,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') {

View File

@ -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');
}
@ -47,6 +52,7 @@ class InstalledVersionsTest extends TestCase
'foo/impl',
'foo/impl2',
'foo/replaced',
'meta/package',
);
$this->assertSame($names, InstalledVersions::getInstalledPackages());
}
@ -69,6 +75,7 @@ class InstalledVersionsTest extends TestCase
array(true, '__root__'),
array(true, 'b/replacer'),
array(false, 'not/there'),
array(true, 'meta/package'),
);
}
@ -187,6 +194,8 @@ class InstalledVersionsTest extends TestCase
$this->assertSame(array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'type' => 'library',
'install_path' => $this->root . '/./',
'aliases' => array(
'1.10.x-dev',
),
@ -198,6 +207,7 @@ class InstalledVersionsTest extends TestCase
public function testGetRawData()
{
$dir = $this->root;
$this->assertSame(require __DIR__.'/Repository/Fixtures/installed.php', InstalledVersions::getRawData());
}
@ -222,4 +232,24 @@ 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'));
}
public function testGetInstallPath()
{
$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'));
}
}

View File

@ -119,4 +119,9 @@ class InstallationManagerMock extends InstallationManager
{
// noop
}
public function getInstalledPackagesByType()
{
return $this->installed;
}
}

View File

@ -12,12 +12,15 @@
namespace Composer\Test\Repository;
use Composer\Package\RootPackageInterface;
use Composer\Repository\FilesystemRepository;
use Composer\Test\TestCase;
use Composer\Json\JsonFile;
class FilesystemRepositoryTest extends TestCase
{
private $root;
public function testRepositoryRead()
{
$json = $this->createJsonFileMock();
@ -121,6 +124,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');
@ -152,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');

View File

@ -14,6 +14,9 @@ return array(
'root' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'type' => 'library',
// @phpstan-ignore-next-line
'install_path' => $dir . '/./',
'aliases' => array(
'1.10.x-dev',
),
@ -25,44 +28,58 @@ return array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'type' => 'library',
// @phpstan-ignore-next-line
'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 . '/vendor/a/provider',
'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 . '/vendor/a/provider2',
'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 . '/vendor/b/replacer',
'aliases' => array(),
'reference' => null,
'dev-requirement' => false,
'dev_requirement' => false,
),
'c/c' => array(
'pretty_version' => '3.0',
'version' => '3.0.0.0',
'type' => 'library',
'install_path' => '/foo/bar/vendor/c/c',
'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',
@ -71,7 +88,7 @@ return array(
),
),
'foo/impl2' => array(
'dev-requirement' => false,
'dev_requirement' => false,
'provided' => array(
'2.0',
),
@ -80,10 +97,19 @@ return array(
),
),
'foo/replaced' => array(
'dev-requirement' => false,
'dev_requirement' => false,
'replaced' => 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,
)
),
);