InstalledVersions.php 17 KB

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