comparison vendor/wikimedia/composer-merge-plugin/src/Merge/ExtraPackage.php @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:c75dbcec494b
1 <?php
2 /**
3 * This file is part of the Composer Merge plugin.
4 *
5 * Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
6 *
7 * This software may be modified and distributed under the terms of the MIT
8 * license. See the LICENSE file for details.
9 */
10
11 namespace Wikimedia\Composer\Merge;
12
13 use Wikimedia\Composer\Logger;
14
15 use Composer\Composer;
16 use Composer\Json\JsonFile;
17 use Composer\Package\BasePackage;
18 use Composer\Package\CompletePackage;
19 use Composer\Package\Link;
20 use Composer\Package\Loader\ArrayLoader;
21 use Composer\Package\RootAliasPackage;
22 use Composer\Package\RootPackage;
23 use Composer\Package\RootPackageInterface;
24 use Composer\Package\Version\VersionParser;
25 use UnexpectedValueException;
26
27 /**
28 * Processing for a composer.json file that will be merged into
29 * a RootPackageInterface
30 *
31 * @author Bryan Davis <bd808@bd808.com>
32 */
33 class ExtraPackage
34 {
35
36 /**
37 * @var Composer $composer
38 */
39 protected $composer;
40
41 /**
42 * @var Logger $logger
43 */
44 protected $logger;
45
46 /**
47 * @var string $path
48 */
49 protected $path;
50
51 /**
52 * @var array $json
53 */
54 protected $json;
55
56 /**
57 * @var CompletePackage $package
58 */
59 protected $package;
60
61 /**
62 * @var VersionParser $versionParser
63 */
64 protected $versionParser;
65
66 /**
67 * @param string $path Path to composer.json file
68 * @param Composer $composer
69 * @param Logger $logger
70 */
71 public function __construct($path, Composer $composer, Logger $logger)
72 {
73 $this->path = $path;
74 $this->composer = $composer;
75 $this->logger = $logger;
76 $this->json = $this->readPackageJson($path);
77 $this->package = $this->loadPackage($this->json);
78 $this->versionParser = new VersionParser();
79 }
80
81 /**
82 * Get list of additional packages to include if precessing recursively.
83 *
84 * @return array
85 */
86 public function getIncludes()
87 {
88 return isset($this->json['extra']['merge-plugin']['include']) ?
89 $this->json['extra']['merge-plugin']['include'] : array();
90 }
91
92 /**
93 * Get list of additional packages to require if precessing recursively.
94 *
95 * @return array
96 */
97 public function getRequires()
98 {
99 return isset($this->json['extra']['merge-plugin']['require']) ?
100 $this->json['extra']['merge-plugin']['require'] : array();
101 }
102
103 /**
104 * Read the contents of a composer.json style file into an array.
105 *
106 * The package contents are fixed up to be usable to create a Package
107 * object by providing dummy "name" and "version" values if they have not
108 * been provided in the file. This is consistent with the default root
109 * package loading behavior of Composer.
110 *
111 * @param string $path
112 * @return array
113 */
114 protected function readPackageJson($path)
115 {
116 $file = new JsonFile($path);
117 $json = $file->read();
118 if (!isset($json['name'])) {
119 $json['name'] = 'merge-plugin/' .
120 strtr($path, DIRECTORY_SEPARATOR, '-');
121 }
122 if (!isset($json['version'])) {
123 $json['version'] = '1.0.0';
124 }
125 return $json;
126 }
127
128 /**
129 * @param array $json
130 * @return CompletePackage
131 */
132 protected function loadPackage(array $json)
133 {
134 $loader = new ArrayLoader();
135 $package = $loader->load($json);
136 // @codeCoverageIgnoreStart
137 if (!$package instanceof CompletePackage) {
138 throw new UnexpectedValueException(
139 'Expected instance of CompletePackage, got ' .
140 get_class($package)
141 );
142 }
143 // @codeCoverageIgnoreEnd
144 return $package;
145 }
146
147 /**
148 * Merge this package into a RootPackageInterface
149 *
150 * @param RootPackageInterface $root
151 * @param PluginState $state
152 */
153 public function mergeInto(RootPackageInterface $root, PluginState $state)
154 {
155 $this->prependRepositories($root);
156
157 $this->mergeRequires('require', $root, $state);
158
159 $this->mergePackageLinks('conflict', $root);
160 $this->mergePackageLinks('replace', $root);
161 $this->mergePackageLinks('provide', $root);
162
163 $this->mergeSuggests($root);
164
165 $this->mergeAutoload('autoload', $root);
166
167 $this->mergeExtra($root, $state);
168
169 $this->mergeScripts($root, $state);
170
171 if ($state->isDevMode()) {
172 $this->mergeDevInto($root, $state);
173 } else {
174 $this->mergeReferences($root);
175 }
176 }
177
178 /**
179 * Merge just the dev portion into a RootPackageInterface
180 *
181 * @param RootPackageInterface $root
182 * @param PluginState $state
183 */
184 public function mergeDevInto(RootPackageInterface $root, PluginState $state)
185 {
186 $this->mergeRequires('require-dev', $root, $state);
187 $this->mergeAutoload('devAutoload', $root);
188 $this->mergeReferences($root);
189 }
190
191 /**
192 * Add a collection of repositories described by the given configuration
193 * to the given package and the global repository manager.
194 *
195 * @param RootPackageInterface $root
196 */
197 protected function prependRepositories(RootPackageInterface $root)
198 {
199 if (!isset($this->json['repositories'])) {
200 return;
201 }
202 $repoManager = $this->composer->getRepositoryManager();
203 $newRepos = array();
204
205 foreach ($this->json['repositories'] as $repoJson) {
206 if (!isset($repoJson['type'])) {
207 continue;
208 }
209 $this->logger->info("Prepending {$repoJson['type']} repository");
210 $repo = $repoManager->createRepository(
211 $repoJson['type'],
212 $repoJson
213 );
214 $repoManager->prependRepository($repo);
215 $newRepos[] = $repo;
216 }
217
218 $unwrapped = self::unwrapIfNeeded($root, 'setRepositories');
219 $unwrapped->setRepositories(array_merge(
220 $newRepos,
221 $root->getRepositories()
222 ));
223 }
224
225 /**
226 * Merge require or require-dev into a RootPackageInterface
227 *
228 * @param string $type 'require' or 'require-dev'
229 * @param RootPackageInterface $root
230 * @param PluginState $state
231 */
232 protected function mergeRequires(
233 $type,
234 RootPackageInterface $root,
235 PluginState $state
236 ) {
237 $linkType = BasePackage::$supportedLinkTypes[$type];
238 $getter = 'get' . ucfirst($linkType['method']);
239 $setter = 'set' . ucfirst($linkType['method']);
240
241 $requires = $this->package->{$getter}();
242 if (empty($requires)) {
243 return;
244 }
245
246 $this->mergeStabilityFlags($root, $requires);
247
248 $requires = $this->replaceSelfVersionDependencies(
249 $type,
250 $requires,
251 $root
252 );
253
254 $root->{$setter}($this->mergeOrDefer(
255 $type,
256 $root->{$getter}(),
257 $requires,
258 $state
259 ));
260 }
261
262 /**
263 * Merge two collections of package links and collect duplicates for
264 * subsequent processing.
265 *
266 * @param string $type 'require' or 'require-dev'
267 * @param array $origin Primary collection
268 * @param array $merge Additional collection
269 * @param PluginState $state
270 * @return array Merged collection
271 */
272 protected function mergeOrDefer(
273 $type,
274 array $origin,
275 array $merge,
276 $state
277 ) {
278 if ($state->ignoreDuplicateLinks() && $state->replaceDuplicateLinks()) {
279 $this->logger->warning("Both replace and ignore-duplicates are true. These are mutually exclusive.");
280 $this->logger->warning("Duplicate packages will be ignored.");
281 }
282
283 $dups = array();
284 foreach ($merge as $name => $link) {
285 if (isset($origin[$name]) && $state->ignoreDuplicateLinks()) {
286 $this->logger->info("Ignoring duplicate <comment>{$name}</comment>");
287 continue;
288 } elseif (!isset($origin[$name]) || $state->replaceDuplicateLinks()) {
289 $this->logger->info("Merging <comment>{$name}</comment>");
290 $origin[$name] = $link;
291 } else {
292 // Defer to solver.
293 $this->logger->info(
294 "Deferring duplicate <comment>{$name}</comment>"
295 );
296 $dups[] = $link;
297 }
298 }
299 $state->addDuplicateLinks($type, $dups);
300 return $origin;
301 }
302
303 /**
304 * Merge autoload or autoload-dev into a RootPackageInterface
305 *
306 * @param string $type 'autoload' or 'devAutoload'
307 * @param RootPackageInterface $root
308 */
309 protected function mergeAutoload($type, RootPackageInterface $root)
310 {
311 $getter = 'get' . ucfirst($type);
312 $setter = 'set' . ucfirst($type);
313
314 $autoload = $this->package->{$getter}();
315 if (empty($autoload)) {
316 return;
317 }
318
319 $unwrapped = self::unwrapIfNeeded($root, $setter);
320 $unwrapped->{$setter}(array_merge_recursive(
321 $root->{$getter}(),
322 $this->fixRelativePaths($autoload)
323 ));
324 }
325
326 /**
327 * Fix a collection of paths that are relative to this package to be
328 * relative to the base package.
329 *
330 * @param array $paths
331 * @return array
332 */
333 protected function fixRelativePaths(array $paths)
334 {
335 $base = dirname($this->path);
336 $base = ($base === '.') ? '' : "{$base}/";
337
338 array_walk_recursive(
339 $paths,
340 function (&$path) use ($base) {
341 $path = "{$base}{$path}";
342 }
343 );
344 return $paths;
345 }
346
347 /**
348 * Extract and merge stability flags from the given collection of
349 * requires and merge them into a RootPackageInterface
350 *
351 * @param RootPackageInterface $root
352 * @param array $requires
353 */
354 protected function mergeStabilityFlags(
355 RootPackageInterface $root,
356 array $requires
357 ) {
358 $flags = $root->getStabilityFlags();
359 $sf = new StabilityFlags($flags, $root->getMinimumStability());
360
361 $unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags');
362 $unwrapped->setStabilityFlags(array_merge(
363 $flags,
364 $sf->extractAll($requires)
365 ));
366 }
367
368 /**
369 * Merge package links of the given type into a RootPackageInterface
370 *
371 * @param string $type 'conflict', 'replace' or 'provide'
372 * @param RootPackageInterface $root
373 */
374 protected function mergePackageLinks($type, RootPackageInterface $root)
375 {
376 $linkType = BasePackage::$supportedLinkTypes[$type];
377 $getter = 'get' . ucfirst($linkType['method']);
378 $setter = 'set' . ucfirst($linkType['method']);
379
380 $links = $this->package->{$getter}();
381 if (!empty($links)) {
382 $unwrapped = self::unwrapIfNeeded($root, $setter);
383 // @codeCoverageIgnoreStart
384 if ($root !== $unwrapped) {
385 $this->logger->warning(
386 'This Composer version does not support ' .
387 "'{$type}' merging for aliased packages."
388 );
389 }
390 // @codeCoverageIgnoreEnd
391 $unwrapped->{$setter}(array_merge(
392 $root->{$getter}(),
393 $this->replaceSelfVersionDependencies($type, $links, $root)
394 ));
395 }
396 }
397
398 /**
399 * Merge suggested packages into a RootPackageInterface
400 *
401 * @param RootPackageInterface $root
402 */
403 protected function mergeSuggests(RootPackageInterface $root)
404 {
405 $suggests = $this->package->getSuggests();
406 if (!empty($suggests)) {
407 $unwrapped = self::unwrapIfNeeded($root, 'setSuggests');
408 $unwrapped->setSuggests(array_merge(
409 $root->getSuggests(),
410 $suggests
411 ));
412 }
413 }
414
415 /**
416 * Merge extra config into a RootPackageInterface
417 *
418 * @param RootPackageInterface $root
419 * @param PluginState $state
420 */
421 public function mergeExtra(RootPackageInterface $root, PluginState $state)
422 {
423 $extra = $this->package->getExtra();
424 unset($extra['merge-plugin']);
425 if (!$state->shouldMergeExtra() || empty($extra)) {
426 return;
427 }
428
429 $rootExtra = $root->getExtra();
430 $unwrapped = self::unwrapIfNeeded($root, 'setExtra');
431
432 if ($state->replaceDuplicateLinks()) {
433 $unwrapped->setExtra(
434 self::mergeExtraArray($state->shouldMergeExtraDeep(), $rootExtra, $extra)
435 );
436 } else {
437 if (!$state->shouldMergeExtraDeep()) {
438 foreach (array_intersect(
439 array_keys($extra),
440 array_keys($rootExtra)
441 ) as $key) {
442 $this->logger->info(
443 "Ignoring duplicate <comment>{$key}</comment> in ".
444 "<comment>{$this->path}</comment> extra config."
445 );
446 }
447 }
448 $unwrapped->setExtra(
449 self::mergeExtraArray($state->shouldMergeExtraDeep(), $extra, $rootExtra)
450 );
451 }
452 }
453
454 /**
455 * Merge scripts config into a RootPackageInterface
456 *
457 * @param RootPackageInterface $root
458 * @param PluginState $state
459 */
460 public function mergeScripts(RootPackageInterface $root, PluginState $state)
461 {
462 $scripts = $this->package->getScripts();
463 if (!$state->shouldMergeScripts() || empty($scripts)) {
464 return;
465 }
466
467 $rootScripts = $root->getScripts();
468 $unwrapped = self::unwrapIfNeeded($root, 'setScripts');
469
470 if ($state->replaceDuplicateLinks()) {
471 $unwrapped->setScripts(
472 array_merge($rootScripts, $scripts)
473 );
474 } else {
475 $unwrapped->setScripts(
476 array_merge($scripts, $rootScripts)
477 );
478 }
479 }
480
481 /**
482 * Merges two arrays either via arrayMergeDeep or via array_merge.
483 *
484 * @param bool $mergeDeep
485 * @param array $array1
486 * @param array $array2
487 * @return array
488 */
489 public static function mergeExtraArray($mergeDeep, $array1, $array2)
490 {
491 if ($mergeDeep) {
492 return NestedArray::mergeDeep($array1, $array2);
493 }
494
495 return array_merge($array1, $array2);
496 }
497
498 /**
499 * Update Links with a 'self.version' constraint with the root package's
500 * version.
501 *
502 * @param string $type Link type
503 * @param array $links
504 * @param RootPackageInterface $root
505 * @return array
506 */
507 protected function replaceSelfVersionDependencies(
508 $type,
509 array $links,
510 RootPackageInterface $root
511 ) {
512 $linkType = BasePackage::$supportedLinkTypes[$type];
513 $version = $root->getVersion();
514 $prettyVersion = $root->getPrettyVersion();
515 $vp = $this->versionParser;
516
517 $method = 'get' . ucfirst($linkType['method']);
518 $packages = $root->$method();
519
520 return array_map(
521 function ($link) use ($linkType, $version, $prettyVersion, $vp, $packages) {
522 if ('self.version' === $link->getPrettyConstraint()) {
523 if (isset($packages[$link->getSource()])) {
524 /** @var Link $package */
525 $package = $packages[$link->getSource()];
526 return new Link(
527 $link->getSource(),
528 $link->getTarget(),
529 $vp->parseConstraints($package->getConstraint()->getPrettyString()),
530 $linkType['description'],
531 $package->getPrettyConstraint()
532 );
533 }
534
535 return new Link(
536 $link->getSource(),
537 $link->getTarget(),
538 $vp->parseConstraints($version),
539 $linkType['description'],
540 $prettyVersion
541 );
542 }
543 return $link;
544 },
545 $links
546 );
547 }
548
549 /**
550 * Get a full featured Package from a RootPackageInterface.
551 *
552 * In Composer versions before 599ad77 the RootPackageInterface only
553 * defines a sub-set of operations needed by composer-merge-plugin and
554 * RootAliasPackage only implemented those methods defined by the
555 * interface. Most of the unimplemented methods in RootAliasPackage can be
556 * worked around because the getter methods that are implemented proxy to
557 * the aliased package which we can modify by unwrapping. The exception
558 * being modifying the 'conflicts', 'provides' and 'replaces' collections.
559 * We have no way to actually modify those collections unfortunately in
560 * older versions of Composer.
561 *
562 * @param RootPackageInterface $root
563 * @param string $method Method needed
564 * @return RootPackageInterface|RootPackage
565 */
566 public static function unwrapIfNeeded(
567 RootPackageInterface $root,
568 $method = 'setExtra'
569 ) {
570 // @codeCoverageIgnoreStart
571 if ($root instanceof RootAliasPackage &&
572 !method_exists($root, $method)
573 ) {
574 // Unwrap and return the aliased RootPackage.
575 $root = $root->getAliasOf();
576 }
577 // @codeCoverageIgnoreEnd
578 return $root;
579 }
580
581 /**
582 * Update the root packages reference information.
583 *
584 * @param RootPackageInterface $root
585 */
586 protected function mergeReferences(RootPackageInterface $root)
587 {
588 // Merge source reference information for merged packages.
589 // @see RootPackageLoader::load
590 $references = array();
591 $unwrapped = $this->unwrapIfNeeded($root, 'setReferences');
592 foreach (array('require', 'require-dev') as $linkType) {
593 $linkInfo = BasePackage::$supportedLinkTypes[$linkType];
594 $method = 'get'.ucfirst($linkInfo['method']);
595 $links = array();
596 foreach ($unwrapped->$method() as $link) {
597 $links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
598 }
599 $references = $this->extractReferences($links, $references);
600 }
601 $unwrapped->setReferences($references);
602 }
603
604 /**
605 * Extract vcs revision from version constraint (dev-master#abc123.
606 *
607 * @param array $requires
608 * @param array $references
609 * @return array
610 * @see RootPackageLoader::extractReferences()
611 */
612 protected function extractReferences(array $requires, array $references)
613 {
614 foreach ($requires as $reqName => $reqVersion) {
615 $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
616 $stabilityName = VersionParser::parseStability($reqVersion);
617 if (
618 preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match) &&
619 $stabilityName === 'dev'
620 ) {
621 $name = strtolower($reqName);
622 $references[$name] = $match[1];
623 }
624 }
625
626 return $references;
627 }
628 }
629 // vim:sw=4:ts=4:sts=4:et: