blob: b3a4e1611e6e5104c1d7a48947807a69ffddb9e1 [file] [log] [blame]
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +01001<?php
2
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02003/*
4 * This file is part of Composer.
5 *
6 * (c) Nils Adermann <naderman@naderman.de>
7 * Jordi Boggiano <j.boggiano@seld.be>
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010012
13namespace Composer;
14
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020015use Composer\Autoload\ClassLoader;
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010016use Composer\Semver\VersionParser;
17
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020018/**
19 * This class is copied in every Composer installed project and available to all
20 *
21 * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
22 *
23 * To require it's presence, you can require `composer-runtime-api ^2.0`
24 */
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010025class InstalledVersions
26{
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020027 private static $installed;
28 private static $canGetVendors;
29 private static $installedByVendor = array();
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010030
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020031 /**
32 * Returns a list of all package names which are present, either by being installed, replaced or provided
33 *
34 * @return string[]
35 * @psalm-return list<string>
36 */
37 public static function getInstalledPackages()
38 {
39 $packages = array();
40 foreach (self::getInstalled() as $installed) {
41 $packages[] = array_keys($installed['versions']);
42 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010043
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020044 if (1 === \count($packages)) {
45 return $packages[0];
46 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010047
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020048 return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
49 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010050
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020051 /**
52 * Returns a list of all package names with a specific type e.g. 'library'
53 *
54 * @param string $type
55 * @return string[]
56 * @psalm-return list<string>
57 */
58 public static function getInstalledPackagesByType($type)
59 {
60 $packagesByType = array();
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010061
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020062 foreach (self::getInstalled() as $installed) {
63 foreach ($installed['versions'] as $name => $package) {
64 if (isset($package['type']) && $package['type'] === $type) {
65 $packagesByType[] = $name;
66 }
67 }
68 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010069
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020070 return $packagesByType;
71 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010072
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020073 /**
74 * Checks whether the given package is installed
75 *
76 * This also returns true if the package name is provided or replaced by another package
77 *
78 * @param string $packageName
79 * @param bool $includeDevRequirements
80 * @return bool
81 */
82 public static function isInstalled($packageName, $includeDevRequirements = true)
83 {
84 foreach (self::getInstalled() as $installed) {
85 if (isset($installed['versions'][$packageName])) {
86 return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
87 }
88 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010089
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020090 return false;
91 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010092
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020093 /**
94 * Checks whether the given package satisfies a version constraint
95 *
96 * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
97 *
98 * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
99 *
100 * @param VersionParser $parser Install composer/semver to have access to this class and functionality
101 * @param string $packageName
102 * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
103 * @return bool
104 */
105 public static function satisfies(VersionParser $parser, $packageName, $constraint)
106 {
107 $constraint = $parser->parseConstraints($constraint);
108 $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100109
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200110 return $provided->matches($constraint);
111 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100112
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200113 /**
114 * Returns a version constraint representing all the range(s) which are installed for a given package
115 *
116 * It is easier to use this via isInstalled() with the $constraint argument if you need to check
117 * whether a given version of a package is installed, and not just whether it exists
118 *
119 * @param string $packageName
120 * @return string Version constraint usable with composer/semver
121 */
122 public static function getVersionRanges($packageName)
123 {
124 foreach (self::getInstalled() as $installed) {
125 if (!isset($installed['versions'][$packageName])) {
126 continue;
127 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100128
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200129 $ranges = array();
130 if (isset($installed['versions'][$packageName]['pretty_version'])) {
131 $ranges[] = $installed['versions'][$packageName]['pretty_version'];
132 }
133 if (array_key_exists('aliases', $installed['versions'][$packageName])) {
134 $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
135 }
136 if (array_key_exists('replaced', $installed['versions'][$packageName])) {
137 $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
138 }
139 if (array_key_exists('provided', $installed['versions'][$packageName])) {
140 $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
141 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100142
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200143 return implode(' || ', $ranges);
144 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100145
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200146 throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
147 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100148
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200149 /**
150 * @param string $packageName
151 * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
152 */
153 public static function getVersion($packageName)
154 {
155 foreach (self::getInstalled() as $installed) {
156 if (!isset($installed['versions'][$packageName])) {
157 continue;
158 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100159
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200160 if (!isset($installed['versions'][$packageName]['version'])) {
161 return null;
162 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100163
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200164 return $installed['versions'][$packageName]['version'];
165 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100166
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200167 throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
168 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100169
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200170 /**
171 * @param string $packageName
172 * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
173 */
174 public static function getPrettyVersion($packageName)
175 {
176 foreach (self::getInstalled() as $installed) {
177 if (!isset($installed['versions'][$packageName])) {
178 continue;
179 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100180
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200181 if (!isset($installed['versions'][$packageName]['pretty_version'])) {
182 return null;
183 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100184
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200185 return $installed['versions'][$packageName]['pretty_version'];
186 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100187
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200188 throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
189 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100190
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200191 /**
192 * @param string $packageName
193 * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
194 */
195 public static function getReference($packageName)
196 {
197 foreach (self::getInstalled() as $installed) {
198 if (!isset($installed['versions'][$packageName])) {
199 continue;
200 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100201
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200202 if (!isset($installed['versions'][$packageName]['reference'])) {
203 return null;
204 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100205
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200206 return $installed['versions'][$packageName]['reference'];
207 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100208
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200209 throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
210 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100211
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200212 /**
213 * @param string $packageName
214 * @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.
215 */
216 public static function getInstallPath($packageName)
217 {
218 foreach (self::getInstalled() as $installed) {
219 if (!isset($installed['versions'][$packageName])) {
220 continue;
221 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100222
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200223 return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
224 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100225
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200226 throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
227 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100228
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200229 /**
230 * @return array
231 * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}
232 */
233 public static function getRootPackage()
234 {
235 $installed = self::getInstalled();
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100236
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200237 return $installed[0]['root'];
238 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100239
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200240 /**
241 * Returns the raw installed.php data for custom implementations
242 *
243 * @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.
244 * @return array[]
245 * @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}>}
246 */
247 public static function getRawData()
248 {
249 @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);
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100250
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200251 if (null === self::$installed) {
252 // only require the installed.php file if this file is loaded from its dumped location,
253 // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
254 if (substr(__DIR__, -8, 1) !== 'C') {
255 self::$installed = include __DIR__ . '/installed.php';
256 } else {
257 self::$installed = array();
258 }
259 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100260
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200261 return self::$installed;
262 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100263
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200264 /**
265 * Returns the raw data of all installed.php which are currently loaded for custom implementations
266 *
267 * @return array[]
268 * @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}>}>
269 */
270 public static function getAllRawData()
271 {
272 return self::getInstalled();
273 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100274
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200275 /**
276 * Lets you reload the static array from another file
277 *
278 * This is only useful for complex integrations in which a project needs to use
279 * this class but then also needs to execute another project's autoloader in process,
280 * and wants to ensure both projects have access to their version of installed.php.
281 *
282 * A typical case would be PHPUnit, where it would need to make sure it reads all
283 * the data it needs from this class, then call reload() with
284 * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
285 * the project in which it runs can then also use this class safely, without
286 * interference between PHPUnit's dependencies and the project's dependencies.
287 *
288 * @param array[] $data A vendor/composer/installed.php data set
289 * @return void
290 *
291 * @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
292 */
293 public static function reload($data)
294 {
295 self::$installed = $data;
296 self::$installedByVendor = array();
297 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100298
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200299 /**
300 * @return array[]
301 * @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}>}>
302 */
303 private static function getInstalled()
304 {
305 if (null === self::$canGetVendors) {
306 self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
307 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100308
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200309 $installed = array();
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100310
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200311 if (self::$canGetVendors) {
312 foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
313 if (isset(self::$installedByVendor[$vendorDir])) {
314 $installed[] = self::$installedByVendor[$vendorDir];
315 } elseif (is_file($vendorDir.'/composer/installed.php')) {
316 $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
317 if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
318 self::$installed = $installed[count($installed) - 1];
319 }
320 }
321 }
322 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100323
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200324 if (null === self::$installed) {
325 // only require the installed.php file if this file is loaded from its dumped location,
326 // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
327 if (substr(__DIR__, -8, 1) !== 'C') {
328 self::$installed = require __DIR__ . '/installed.php';
329 } else {
330 self::$installed = array();
331 }
332 }
333 $installed[] = self::$installed;
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100334
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200335 return $installed;
336 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100337}