RootPackageLoader.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  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\Package\Loader;
  12. use Composer\Package\BasePackage;
  13. use Composer\Package\AliasPackage;
  14. use Composer\Config;
  15. use Composer\Factory;
  16. use Composer\Package\Version\VersionParser;
  17. use Composer\Repository\RepositoryManager;
  18. use Composer\Repository\Vcs\HgDriver;
  19. use Composer\IO\NullIO;
  20. use Composer\Util\ProcessExecutor;
  21. use Composer\Util\Git as GitUtil;
  22. use Composer\Util\Svn as SvnUtil;
  23. /**
  24. * ArrayLoader built for the sole purpose of loading the root package
  25. *
  26. * Sets additional defaults and loads repositories
  27. *
  28. * @author Jordi Boggiano <j.boggiano@seld.be>
  29. */
  30. class RootPackageLoader extends ArrayLoader
  31. {
  32. private $manager;
  33. private $config;
  34. private $process;
  35. public function __construct(RepositoryManager $manager, Config $config, VersionParser $parser = null, ProcessExecutor $process = null)
  36. {
  37. $this->manager = $manager;
  38. $this->config = $config;
  39. $this->process = $process ?: new ProcessExecutor();
  40. parent::__construct($parser);
  41. }
  42. public function load(array $config, $class = 'Composer\Package\RootPackage')
  43. {
  44. if (!isset($config['name'])) {
  45. $config['name'] = '__root__';
  46. }
  47. $autoVersioned = false;
  48. if (!isset($config['version'])) {
  49. // override with env var if available
  50. if (getenv('COMPOSER_ROOT_VERSION')) {
  51. $version = getenv('COMPOSER_ROOT_VERSION');
  52. } else {
  53. $version = $this->guessVersion($config);
  54. }
  55. if (!$version) {
  56. $version = '1.0.0';
  57. $autoVersioned = true;
  58. }
  59. $config['version'] = $version;
  60. }
  61. $realPackage = $package = parent::load($config, $class);
  62. if ($realPackage instanceof AliasPackage) {
  63. $realPackage = $package->getAliasOf();
  64. }
  65. if ($autoVersioned) {
  66. $realPackage->replaceVersion($realPackage->getVersion(), 'No version set (parsed as 1.0.0)');
  67. }
  68. if (isset($config['minimum-stability'])) {
  69. $realPackage->setMinimumStability(VersionParser::normalizeStability($config['minimum-stability']));
  70. }
  71. $aliases = array();
  72. $stabilityFlags = array();
  73. $references = array();
  74. foreach (array('require', 'require-dev') as $linkType) {
  75. if (isset($config[$linkType])) {
  76. $linkInfo = BasePackage::$supportedLinkTypes[$linkType];
  77. $method = 'get'.ucfirst($linkInfo['method']);
  78. $links = array();
  79. foreach ($realPackage->$method() as $link) {
  80. $links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
  81. }
  82. $aliases = $this->extractAliases($links, $aliases);
  83. $stabilityFlags = $this->extractStabilityFlags($links, $stabilityFlags, $realPackage->getMinimumStability());
  84. $references = $this->extractReferences($links, $references);
  85. }
  86. }
  87. $realPackage->setAliases($aliases);
  88. $realPackage->setStabilityFlags($stabilityFlags);
  89. $realPackage->setReferences($references);
  90. if (isset($config['prefer-stable'])) {
  91. $realPackage->setPreferStable((bool) $config['prefer-stable']);
  92. }
  93. $repos = Factory::createDefaultRepositories(null, $this->config, $this->manager);
  94. foreach ($repos as $repo) {
  95. $this->manager->addRepository($repo);
  96. }
  97. $realPackage->setRepositories($this->config->getRepositories());
  98. return $package;
  99. }
  100. private function extractAliases(array $requires, array $aliases)
  101. {
  102. foreach ($requires as $reqName => $reqVersion) {
  103. if (preg_match('{^([^,\s#]+)(?:#[^ ]+)? +as +([^,\s]+)$}', $reqVersion, $match)) {
  104. $aliases[] = array(
  105. 'package' => strtolower($reqName),
  106. 'version' => $this->versionParser->normalize($match[1], $reqVersion),
  107. 'alias' => $match[2],
  108. 'alias_normalized' => $this->versionParser->normalize($match[2], $reqVersion),
  109. );
  110. }
  111. }
  112. return $aliases;
  113. }
  114. private function extractStabilityFlags(array $requires, array $stabilityFlags, $minimumStability)
  115. {
  116. $stabilities = BasePackage::$stabilities;
  117. $minimumStability = $stabilities[$minimumStability];
  118. foreach ($requires as $reqName => $reqVersion) {
  119. // parse explicit stability flags to the most unstable
  120. if (preg_match('{^[^@]*?@('.implode('|', array_keys($stabilities)).')$}i', $reqVersion, $match)) {
  121. $name = strtolower($reqName);
  122. $stability = $stabilities[VersionParser::normalizeStability($match[1])];
  123. if (isset($stabilityFlags[$name]) && $stabilityFlags[$name] > $stability) {
  124. continue;
  125. }
  126. $stabilityFlags[$name] = $stability;
  127. continue;
  128. }
  129. // infer flags for requirements that have an explicit -dev or -beta version specified but only
  130. // for those that are more unstable than the minimumStability or existing flags
  131. $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
  132. if (preg_match('{^[^,\s@]+$}', $reqVersion) && 'stable' !== ($stabilityName = VersionParser::parseStability($reqVersion))) {
  133. $name = strtolower($reqName);
  134. $stability = $stabilities[$stabilityName];
  135. if ((isset($stabilityFlags[$name]) && $stabilityFlags[$name] > $stability) || ($minimumStability > $stability)) {
  136. continue;
  137. }
  138. $stabilityFlags[$name] = $stability;
  139. }
  140. }
  141. return $stabilityFlags;
  142. }
  143. private function extractReferences(array $requires, array $references)
  144. {
  145. foreach ($requires as $reqName => $reqVersion) {
  146. $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
  147. if (preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match) && 'dev' === ($stabilityName = VersionParser::parseStability($reqVersion))) {
  148. $name = strtolower($reqName);
  149. $references[$name] = $match[1];
  150. }
  151. }
  152. return $references;
  153. }
  154. private function guessVersion(array $config)
  155. {
  156. if (function_exists('proc_open')) {
  157. $version = $this->guessGitVersion($config);
  158. if (null !== $version) {
  159. return $version;
  160. }
  161. $version = $this->guessHgVersion($config);
  162. if (null !== $version) {
  163. return $version;
  164. }
  165. return $this->guessSvnVersion($config);
  166. }
  167. }
  168. private function guessGitVersion(array $config)
  169. {
  170. GitUtil::cleanEnv();
  171. // try to fetch current version from git tags
  172. if (0 === $this->process->execute('git describe --exact-match --tags', $output)) {
  173. try {
  174. return $this->versionParser->normalize(trim($output));
  175. } catch (\Exception $e) {
  176. }
  177. }
  178. // try to fetch current version from git branch
  179. if (0 === $this->process->execute('git branch --no-color --no-abbrev -v', $output)) {
  180. $branches = array();
  181. $isFeatureBranch = false;
  182. $version = null;
  183. // find current branch and collect all branch names
  184. foreach ($this->process->splitLines($output) as $branch) {
  185. if ($branch && preg_match('{^(?:\* ) *(\(no branch\)|\(detached from \S+\)|\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
  186. if ($match[1] === '(no branch)' || substr($match[1], 0, 10) === '(detached ') {
  187. $version = 'dev-'.$match[2];
  188. $isFeatureBranch = true;
  189. } else {
  190. $version = $this->versionParser->normalizeBranch($match[1]);
  191. $isFeatureBranch = 0 === strpos($version, 'dev-');
  192. if ('9999999-dev' === $version) {
  193. $version = 'dev-'.$match[1];
  194. }
  195. }
  196. }
  197. if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) {
  198. if (preg_match('{^(?:\* )? *(\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
  199. $branches[] = $match[1];
  200. }
  201. }
  202. }
  203. if (!$isFeatureBranch) {
  204. return $version;
  205. }
  206. // try to find the best (nearest) version branch to assume this feature's version
  207. $version = $this->guessFeatureVersion($config, $version, $branches, 'git rev-list %candidate%..%branch%');
  208. return $version;
  209. }
  210. }
  211. private function guessHgVersion(array $config)
  212. {
  213. // try to fetch current version from hg branch
  214. if (0 === $this->process->execute('hg branch', $output)) {
  215. $branch = trim($output);
  216. $version = $this->versionParser->normalizeBranch($branch);
  217. $isFeatureBranch = 0 === strpos($version, 'dev-');
  218. if ('9999999-dev' === $version) {
  219. $version = 'dev-'.$branch;
  220. }
  221. if (!$isFeatureBranch) {
  222. return $version;
  223. }
  224. // re-use the HgDriver to fetch branches (this properly includes bookmarks)
  225. $config = array('url' => getcwd());
  226. $driver = new HgDriver($config, new NullIO(), $this->config, $this->process);
  227. $branches = array_keys($driver->getBranches());
  228. // try to find the best (nearest) version branch to assume this feature's version
  229. $version = $this->guessFeatureVersion($config, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"');
  230. return $version;
  231. }
  232. }
  233. private function guessFeatureVersion(array $config, $version, array $branches, $scmCmdline)
  234. {
  235. // ignore feature branches if they have no branch-alias or self.version is used
  236. // and find the branch they came from to use as a version instead
  237. if ((isset($config['extra']['branch-alias']) && !isset($config['extra']['branch-alias'][$version]))
  238. || strpos(json_encode($config), '"self.version"')
  239. ) {
  240. $branch = preg_replace('{^dev-}', '', $version);
  241. $length = PHP_INT_MAX;
  242. $nonFeatureBranches = '';
  243. if (!empty($config['non-feature-branches'])) {
  244. $nonFeatureBranches = implode('|', $config['non-feature-branches']);
  245. }
  246. foreach ($branches as $candidate) {
  247. // return directly, if branch is configured to be non-feature branch
  248. if ($candidate === $branch && preg_match('{^(' . $nonFeatureBranches . ')$}', $candidate)) {
  249. return $version;
  250. }
  251. // do not compare against other feature branches
  252. if ($candidate === $branch || !preg_match('{^(master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) {
  253. continue;
  254. }
  255. $cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline);
  256. if (0 !== $this->process->execute($cmdLine, $output)) {
  257. continue;
  258. }
  259. if (strlen($output) < $length) {
  260. $length = strlen($output);
  261. $version = $this->versionParser->normalizeBranch($candidate);
  262. if ('9999999-dev' === $version) {
  263. $version = 'dev-'.$match[1];
  264. }
  265. }
  266. }
  267. }
  268. return $version;
  269. }
  270. private function guessSvnVersion(array $config)
  271. {
  272. SvnUtil::cleanEnv();
  273. // try to fetch current version from svn
  274. if (0 === $this->process->execute('svn info --xml', $output)) {
  275. $trunkPath = isset($config['trunk-path']) ? preg_quote($config['trunk-path'], '#') : 'trunk';
  276. $branchesPath = isset($config['branches-path']) ? preg_quote($config['branches-path'], '#') : 'branches';
  277. $tagsPath = isset($config['tags-path']) ? preg_quote($config['tags-path'], '#') : 'tags';
  278. $urlPattern = '#<url>.*/('.$trunkPath.'|('.$branchesPath.'|'. $tagsPath .')/(.*))</url>#';
  279. if (preg_match($urlPattern, $output, $matches)) {
  280. if (isset($matches[2]) && ($branchesPath === $matches[2] || $tagsPath === $matches[2])) {
  281. // we are in a branches path
  282. $version = $this->versionParser->normalizeBranch($matches[3]);
  283. if ('9999999-dev' === $version) {
  284. $version = 'dev-'.$matches[3];
  285. }
  286. return $version;
  287. }
  288. return $this->versionParser->normalize(trim($matches[1]));
  289. }
  290. }
  291. }
  292. }