InstalledVersions.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer;
  12. use Composer\Autoload\ClassLoader;
  13. use Composer\Semver\VersionParser;
  14. /**
  15. * This class is copied in every Composer installed project and available to all
  16. *
  17. * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
  18. *
  19. * To require its presence, you can require `composer-runtime-api ^2.0`
  20. *
  21. * @final
  22. */
  23. class InstalledVersions
  24. {
  25. /**
  26. * @var mixed[]|null
  27. * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
  28. */
  29. private static $installed;
  30. /**
  31. * @var bool
  32. */
  33. private static $installedIsLocalDir;
  34. /**
  35. * @var bool|null
  36. */
  37. private static $canGetVendors;
  38. /**
  39. * @var array[]
  40. * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
  41. */
  42. private static $installedByVendor = array();
  43. /**
  44. * Returns a list of all package names which are present, either by being installed, replaced or provided
  45. *
  46. * @return string[]
  47. * @psalm-return list<string>
  48. */
  49. public static function getInstalledPackages()
  50. {
  51. $packages = array();
  52. foreach (self::getInstalled() as $installed) {
  53. $packages[] = array_keys($installed['versions']);
  54. }
  55. if (1 === \count($packages)) {
  56. return $packages[0];
  57. }
  58. return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
  59. }
  60. /**
  61. * Returns a list of all package names with a specific type e.g. 'library'
  62. *
  63. * @param string $type
  64. * @return string[]
  65. * @psalm-return list<string>
  66. */
  67. public static function getInstalledPackagesByType($type)
  68. {
  69. $packagesByType = array();
  70. foreach (self::getInstalled() as $installed) {
  71. foreach ($installed['versions'] as $name => $package) {
  72. if (isset($package['type']) && $package['type'] === $type) {
  73. $packagesByType[] = $name;
  74. }
  75. }
  76. }
  77. return $packagesByType;
  78. }
  79. /**
  80. * Checks whether the given package is installed
  81. *
  82. * This also returns true if the package name is provided or replaced by another package
  83. *
  84. * @param string $packageName
  85. * @param bool $includeDevRequirements
  86. * @return bool
  87. */
  88. public static function isInstalled($packageName, $includeDevRequirements = true)
  89. {
  90. foreach (self::getInstalled() as $installed) {
  91. if (isset($installed['versions'][$packageName])) {
  92. return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
  93. }
  94. }
  95. return false;
  96. }
  97. /**
  98. * Checks whether the given package satisfies a version constraint
  99. *
  100. * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
  101. *
  102. * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
  103. *
  104. * @param VersionParser $parser Install composer/semver to have access to this class and functionality
  105. * @param string $packageName
  106. * @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
  107. * @return bool
  108. */
  109. public static function satisfies(VersionParser $parser, $packageName, $constraint)
  110. {
  111. $constraint = $parser->parseConstraints((string) $constraint);
  112. $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
  113. return $provided->matches($constraint);
  114. }
  115. /**
  116. * Returns a version constraint representing all the range(s) which are installed for a given package
  117. *
  118. * It is easier to use this via isInstalled() with the $constraint argument if you need to check
  119. * whether a given version of a package is installed, and not just whether it exists
  120. *
  121. * @param string $packageName
  122. * @return string Version constraint usable with composer/semver
  123. */
  124. public static function getVersionRanges($packageName)
  125. {
  126. foreach (self::getInstalled() as $installed) {
  127. if (!isset($installed['versions'][$packageName])) {
  128. continue;
  129. }
  130. $ranges = array();
  131. if (isset($installed['versions'][$packageName]['pretty_version'])) {
  132. $ranges[] = $installed['versions'][$packageName]['pretty_version'];
  133. }
  134. if (array_key_exists('aliases', $installed['versions'][$packageName])) {
  135. $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
  136. }
  137. if (array_key_exists('replaced', $installed['versions'][$packageName])) {
  138. $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
  139. }
  140. if (array_key_exists('provided', $installed['versions'][$packageName])) {
  141. $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
  142. }
  143. return implode(' || ', $ranges);
  144. }
  145. throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
  146. }
  147. /**
  148. * @param string $packageName
  149. * @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
  150. */
  151. public static function getVersion($packageName)
  152. {
  153. foreach (self::getInstalled() as $installed) {
  154. if (!isset($installed['versions'][$packageName])) {
  155. continue;
  156. }
  157. if (!isset($installed['versions'][$packageName]['version'])) {
  158. return null;
  159. }
  160. return $installed['versions'][$packageName]['version'];
  161. }
  162. throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
  163. }
  164. /**
  165. * @param string $packageName
  166. * @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
  167. */
  168. public static function getPrettyVersion($packageName)
  169. {
  170. foreach (self::getInstalled() as $installed) {
  171. if (!isset($installed['versions'][$packageName])) {
  172. continue;
  173. }
  174. if (!isset($installed['versions'][$packageName]['pretty_version'])) {
  175. return null;
  176. }
  177. return $installed['versions'][$packageName]['pretty_version'];
  178. }
  179. throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
  180. }
  181. /**
  182. * @param string $packageName
  183. * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
  184. */
  185. public static function getReference($packageName)
  186. {
  187. foreach (self::getInstalled() as $installed) {
  188. if (!isset($installed['versions'][$packageName])) {
  189. continue;
  190. }
  191. if (!isset($installed['versions'][$packageName]['reference'])) {
  192. return null;
  193. }
  194. return $installed['versions'][$packageName]['reference'];
  195. }
  196. throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
  197. }
  198. /**
  199. * @param string $packageName
  200. * @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.
  201. */
  202. public static function getInstallPath($packageName)
  203. {
  204. foreach (self::getInstalled() as $installed) {
  205. if (!isset($installed['versions'][$packageName])) {
  206. continue;
  207. }
  208. return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
  209. }
  210. throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
  211. }
  212. /**
  213. * @return array
  214. * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
  215. */
  216. public static function getRootPackage()
  217. {
  218. $installed = self::getInstalled();
  219. return $installed[0]['root'];
  220. }
  221. /**
  222. * Returns the raw installed.php data for custom implementations
  223. *
  224. * @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.
  225. * @return array[]
  226. * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
  227. */
  228. public static function getRawData()
  229. {
  230. @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);
  231. if (null === self::$installed) {
  232. // only require the installed.php file if this file is loaded from its dumped location,
  233. // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
  234. if (substr(__DIR__, -8, 1) !== 'C') {
  235. self::$installed = include __DIR__ . '/installed.php';
  236. } else {
  237. self::$installed = array();
  238. }
  239. }
  240. return self::$installed;
  241. }
  242. /**
  243. * Returns the raw data of all installed.php which are currently loaded for custom implementations
  244. *
  245. * @return array[]
  246. * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
  247. */
  248. public static function getAllRawData()
  249. {
  250. return self::getInstalled();
  251. }
  252. /**
  253. * Lets you reload the static array from another file
  254. *
  255. * This is only useful for complex integrations in which a project needs to use
  256. * this class but then also needs to execute another project's autoloader in process,
  257. * and wants to ensure both projects have access to their version of installed.php.
  258. *
  259. * A typical case would be PHPUnit, where it would need to make sure it reads all
  260. * the data it needs from this class, then call reload() with
  261. * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
  262. * the project in which it runs can then also use this class safely, without
  263. * interference between PHPUnit's dependencies and the project's dependencies.
  264. *
  265. * @param array[] $data A vendor/composer/installed.php data set
  266. * @return void
  267. *
  268. * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
  269. */
  270. public static function reload($data)
  271. {
  272. self::$installed = $data;
  273. self::$installedByVendor = array();
  274. // when using reload, we disable the duplicate protection to ensure that self::$installed data is
  275. // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
  276. // so we have to assume it does not, and that may result in duplicate data being returned when listing
  277. // all installed packages for example
  278. self::$installedIsLocalDir = false;
  279. }
  280. /**
  281. * @return array[]
  282. * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
  283. */
  284. private static function getInstalled()
  285. {
  286. if (null === self::$canGetVendors) {
  287. self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
  288. }
  289. $installed = array();
  290. $copiedLocalDir = false;
  291. if (self::$canGetVendors) {
  292. $selfDir = strtr(__DIR__, '\\', '/');
  293. foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
  294. $vendorDir = strtr($vendorDir, '\\', '/');
  295. if (isset(self::$installedByVendor[$vendorDir])) {
  296. $installed[] = self::$installedByVendor[$vendorDir];
  297. } elseif (is_file($vendorDir.'/composer/installed.php')) {
  298. /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
  299. $required = require $vendorDir.'/composer/installed.php';
  300. self::$installedByVendor[$vendorDir] = $required;
  301. $installed[] = $required;
  302. if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
  303. self::$installed = $required;
  304. self::$installedIsLocalDir = true;
  305. }
  306. }
  307. if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
  308. $copiedLocalDir = true;
  309. }
  310. }
  311. }
  312. if (null === self::$installed) {
  313. // only require the installed.php file if this file is loaded from its dumped location,
  314. // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
  315. if (substr(__DIR__, -8, 1) !== 'C') {
  316. /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
  317. $required = require __DIR__ . '/installed.php';
  318. self::$installed = $required;
  319. } else {
  320. self::$installed = array();
  321. }
  322. }
  323. if (self::$installed !== array() && !$copiedLocalDir) {
  324. $installed[] = self::$installed;
  325. }
  326. return $installed;
  327. }
  328. }