Mercurial > hg > cmmr2012-drupal-site
comparison vendor/wikimedia/composer-merge-plugin/src/MergePlugin.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; | |
12 | |
13 use Wikimedia\Composer\Merge\ExtraPackage; | |
14 use Wikimedia\Composer\Merge\MissingFileException; | |
15 use Wikimedia\Composer\Merge\PluginState; | |
16 | |
17 use Composer\Composer; | |
18 use Composer\DependencyResolver\Operation\InstallOperation; | |
19 use Composer\EventDispatcher\Event as BaseEvent; | |
20 use Composer\EventDispatcher\EventSubscriberInterface; | |
21 use Composer\Factory; | |
22 use Composer\Installer; | |
23 use Composer\Installer\InstallerEvent; | |
24 use Composer\Installer\InstallerEvents; | |
25 use Composer\Installer\PackageEvent; | |
26 use Composer\Installer\PackageEvents; | |
27 use Composer\IO\IOInterface; | |
28 use Composer\Package\RootPackageInterface; | |
29 use Composer\Plugin\PluginInterface; | |
30 use Composer\Script\Event as ScriptEvent; | |
31 use Composer\Script\ScriptEvents; | |
32 | |
33 /** | |
34 * Composer plugin that allows merging multiple composer.json files. | |
35 * | |
36 * When installed, this plugin will look for a "merge-plugin" key in the | |
37 * composer configuration's "extra" section. The value for this key is | |
38 * a set of options configuring the plugin. | |
39 * | |
40 * An "include" setting is required. The value of this setting can be either | |
41 * a single value or an array of values. Each value is treated as a glob() | |
42 * pattern identifying additional composer.json style configuration files to | |
43 * merge into the configuration for the current compser execution. | |
44 * | |
45 * The "autoload", "autoload-dev", "conflict", "provide", "replace", | |
46 * "repositories", "require", "require-dev", and "suggest" sections of the | |
47 * found configuration files will be merged into the root package | |
48 * configuration as though they were directly included in the top-level | |
49 * composer.json file. | |
50 * | |
51 * If included files specify conflicting package versions for "require" or | |
52 * "require-dev", the normal Composer dependency solver process will be used | |
53 * to attempt to resolve the conflict. Specifying the 'replace' key as true will | |
54 * change this default behaviour so that the last-defined version of a package | |
55 * will win, allowing for force-overrides of package defines. | |
56 * | |
57 * By default the "extra" section is not merged. This can be enabled by | |
58 * setitng the 'merge-extra' key to true. In normal mode, when the same key is | |
59 * found in both the original and the imported extra section, the version in | |
60 * the original config is used and the imported version is skipped. If | |
61 * 'replace' mode is active, this behaviour changes so the imported version of | |
62 * the key is used, replacing the version in the original config. | |
63 * | |
64 * | |
65 * @code | |
66 * { | |
67 * "require": { | |
68 * "wikimedia/composer-merge-plugin": "dev-master" | |
69 * }, | |
70 * "extra": { | |
71 * "merge-plugin": { | |
72 * "include": [ | |
73 * "composer.local.json" | |
74 * ] | |
75 * } | |
76 * } | |
77 * } | |
78 * @endcode | |
79 * | |
80 * @author Bryan Davis <bd808@bd808.com> | |
81 */ | |
82 class MergePlugin implements PluginInterface, EventSubscriberInterface | |
83 { | |
84 | |
85 /** | |
86 * Offical package name | |
87 */ | |
88 const PACKAGE_NAME = 'wikimedia/composer-merge-plugin'; | |
89 | |
90 /** | |
91 * Name of the composer 1.1 init event. | |
92 */ | |
93 const COMPAT_PLUGINEVENTS_INIT = 'init'; | |
94 | |
95 /** | |
96 * Priority that plugin uses to register callbacks. | |
97 */ | |
98 const CALLBACK_PRIORITY = 50000; | |
99 | |
100 /** | |
101 * @var Composer $composer | |
102 */ | |
103 protected $composer; | |
104 | |
105 /** | |
106 * @var PluginState $state | |
107 */ | |
108 protected $state; | |
109 | |
110 /** | |
111 * @var Logger $logger | |
112 */ | |
113 protected $logger; | |
114 | |
115 /** | |
116 * Files that have already been fully processed | |
117 * | |
118 * @var string[] $loaded | |
119 */ | |
120 protected $loaded = array(); | |
121 | |
122 /** | |
123 * Files that have already been partially processed | |
124 * | |
125 * @var string[] $loadedNoDev | |
126 */ | |
127 protected $loadedNoDev = array(); | |
128 | |
129 /** | |
130 * {@inheritdoc} | |
131 */ | |
132 public function activate(Composer $composer, IOInterface $io) | |
133 { | |
134 $this->composer = $composer; | |
135 $this->state = new PluginState($this->composer); | |
136 $this->logger = new Logger('merge-plugin', $io); | |
137 } | |
138 | |
139 /** | |
140 * {@inheritdoc} | |
141 */ | |
142 public static function getSubscribedEvents() | |
143 { | |
144 return array( | |
145 // Use our own constant to make this event optional. Once | |
146 // composer-1.1 is required, this can use PluginEvents::INIT | |
147 // instead. | |
148 self::COMPAT_PLUGINEVENTS_INIT => | |
149 array('onInit', self::CALLBACK_PRIORITY), | |
150 InstallerEvents::PRE_DEPENDENCIES_SOLVING => | |
151 array('onDependencySolve', self::CALLBACK_PRIORITY), | |
152 PackageEvents::POST_PACKAGE_INSTALL => | |
153 array('onPostPackageInstall', self::CALLBACK_PRIORITY), | |
154 ScriptEvents::POST_INSTALL_CMD => | |
155 array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY), | |
156 ScriptEvents::POST_UPDATE_CMD => | |
157 array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY), | |
158 ScriptEvents::PRE_AUTOLOAD_DUMP => | |
159 array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), | |
160 ScriptEvents::PRE_INSTALL_CMD => | |
161 array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), | |
162 ScriptEvents::PRE_UPDATE_CMD => | |
163 array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), | |
164 ); | |
165 } | |
166 | |
167 /** | |
168 * Handle an event callback for initialization. | |
169 * | |
170 * @param \Composer\EventDispatcher\Event $event | |
171 */ | |
172 public function onInit(BaseEvent $event) | |
173 { | |
174 $this->state->loadSettings(); | |
175 // It is not possible to know if the user specified --dev or --no-dev | |
176 // so assume it is false. The dev section will be merged later when | |
177 // the other events fire. | |
178 $this->state->setDevMode(false); | |
179 $this->mergeFiles($this->state->getIncludes(), false); | |
180 $this->mergeFiles($this->state->getRequires(), true); | |
181 } | |
182 | |
183 /** | |
184 * Handle an event callback for an install, update or dump command by | |
185 * checking for "merge-plugin" in the "extra" data and merging package | |
186 * contents if found. | |
187 * | |
188 * @param ScriptEvent $event | |
189 */ | |
190 public function onInstallUpdateOrDump(ScriptEvent $event) | |
191 { | |
192 $this->state->loadSettings(); | |
193 $this->state->setDevMode($event->isDevMode()); | |
194 $this->mergeFiles($this->state->getIncludes(), false); | |
195 $this->mergeFiles($this->state->getRequires(), true); | |
196 | |
197 if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) { | |
198 $this->state->setDumpAutoloader(true); | |
199 $flags = $event->getFlags(); | |
200 if (isset($flags['optimize'])) { | |
201 $this->state->setOptimizeAutoloader($flags['optimize']); | |
202 } | |
203 } | |
204 } | |
205 | |
206 /** | |
207 * Find configuration files matching the configured glob patterns and | |
208 * merge their contents with the master package. | |
209 * | |
210 * @param array $patterns List of files/glob patterns | |
211 * @param bool $required Are the patterns required to match files? | |
212 * @throws MissingFileException when required and a pattern returns no | |
213 * results | |
214 */ | |
215 protected function mergeFiles(array $patterns, $required = false) | |
216 { | |
217 $root = $this->composer->getPackage(); | |
218 | |
219 $files = array_map( | |
220 function ($files, $pattern) use ($required) { | |
221 if ($required && !$files) { | |
222 throw new MissingFileException( | |
223 "merge-plugin: No files matched required '{$pattern}'" | |
224 ); | |
225 } | |
226 return $files; | |
227 }, | |
228 array_map('glob', $patterns), | |
229 $patterns | |
230 ); | |
231 | |
232 foreach (array_reduce($files, 'array_merge', array()) as $path) { | |
233 $this->mergeFile($root, $path); | |
234 } | |
235 } | |
236 | |
237 /** | |
238 * Read a JSON file and merge its contents | |
239 * | |
240 * @param RootPackageInterface $root | |
241 * @param string $path | |
242 */ | |
243 protected function mergeFile(RootPackageInterface $root, $path) | |
244 { | |
245 if (isset($this->loaded[$path]) || | |
246 (isset($this->loadedNoDev[$path]) && !$this->state->isDevMode()) | |
247 ) { | |
248 $this->logger->debug( | |
249 "Already merged <comment>$path</comment> completely" | |
250 ); | |
251 return; | |
252 } | |
253 | |
254 $package = new ExtraPackage($path, $this->composer, $this->logger); | |
255 | |
256 if (isset($this->loadedNoDev[$path])) { | |
257 $this->logger->info( | |
258 "Loading -dev sections of <comment>{$path}</comment>..." | |
259 ); | |
260 $package->mergeDevInto($root, $this->state); | |
261 } else { | |
262 $this->logger->info("Loading <comment>{$path}</comment>..."); | |
263 $package->mergeInto($root, $this->state); | |
264 } | |
265 | |
266 if ($this->state->isDevMode()) { | |
267 $this->loaded[$path] = true; | |
268 } else { | |
269 $this->loadedNoDev[$path] = true; | |
270 } | |
271 | |
272 if ($this->state->recurseIncludes()) { | |
273 $this->mergeFiles($package->getIncludes(), false); | |
274 $this->mergeFiles($package->getRequires(), true); | |
275 } | |
276 } | |
277 | |
278 /** | |
279 * Handle an event callback for pre-dependency solving phase of an install | |
280 * or update by adding any duplicate package dependencies found during | |
281 * initial merge processing to the request that will be processed by the | |
282 * dependency solver. | |
283 * | |
284 * @param InstallerEvent $event | |
285 */ | |
286 public function onDependencySolve(InstallerEvent $event) | |
287 { | |
288 $request = $event->getRequest(); | |
289 foreach ($this->state->getDuplicateLinks('require') as $link) { | |
290 $this->logger->info( | |
291 "Adding dependency <comment>{$link}</comment>" | |
292 ); | |
293 $request->install($link->getTarget(), $link->getConstraint()); | |
294 } | |
295 | |
296 // Issue #113: Check devMode of event rather than our global state. | |
297 // Composer fires the PRE_DEPENDENCIES_SOLVING event twice for | |
298 // `--no-dev` operations to decide which packages are dev only | |
299 // requirements. | |
300 if ($this->state->shouldMergeDev() && $event->isDevMode()) { | |
301 foreach ($this->state->getDuplicateLinks('require-dev') as $link) { | |
302 $this->logger->info( | |
303 "Adding dev dependency <comment>{$link}</comment>" | |
304 ); | |
305 $request->install($link->getTarget(), $link->getConstraint()); | |
306 } | |
307 } | |
308 } | |
309 | |
310 /** | |
311 * Handle an event callback following installation of a new package by | |
312 * checking to see if the package that was installed was our plugin. | |
313 * | |
314 * @param PackageEvent $event | |
315 */ | |
316 public function onPostPackageInstall(PackageEvent $event) | |
317 { | |
318 $op = $event->getOperation(); | |
319 if ($op instanceof InstallOperation) { | |
320 $package = $op->getPackage()->getName(); | |
321 if ($package === self::PACKAGE_NAME) { | |
322 $this->logger->info('composer-merge-plugin installed'); | |
323 $this->state->setFirstInstall(true); | |
324 $this->state->setLocked( | |
325 $event->getComposer()->getLocker()->isLocked() | |
326 ); | |
327 } | |
328 } | |
329 } | |
330 | |
331 /** | |
332 * Handle an event callback following an install or update command. If our | |
333 * plugin was installed during the run then trigger an update command to | |
334 * process any merge-patterns in the current config. | |
335 * | |
336 * @param ScriptEvent $event | |
337 */ | |
338 public function onPostInstallOrUpdate(ScriptEvent $event) | |
339 { | |
340 // @codeCoverageIgnoreStart | |
341 if ($this->state->isFirstInstall()) { | |
342 $this->state->setFirstInstall(false); | |
343 $this->logger->info( | |
344 '<comment>' . | |
345 'Running additional update to apply merge settings' . | |
346 '</comment>' | |
347 ); | |
348 | |
349 $config = $this->composer->getConfig(); | |
350 | |
351 $preferSource = $config->get('preferred-install') == 'source'; | |
352 $preferDist = $config->get('preferred-install') == 'dist'; | |
353 | |
354 $installer = Installer::create( | |
355 $event->getIO(), | |
356 // Create a new Composer instance to ensure full processing of | |
357 // the merged files. | |
358 Factory::create($event->getIO(), null, false) | |
359 ); | |
360 | |
361 $installer->setPreferSource($preferSource); | |
362 $installer->setPreferDist($preferDist); | |
363 $installer->setDevMode($event->isDevMode()); | |
364 $installer->setDumpAutoloader($this->state->shouldDumpAutoloader()); | |
365 $installer->setOptimizeAutoloader( | |
366 $this->state->shouldOptimizeAutoloader() | |
367 ); | |
368 | |
369 if ($this->state->forceUpdate()) { | |
370 // Force update mode so that new packages are processed rather | |
371 // than just telling the user that composer.json and | |
372 // composer.lock don't match. | |
373 $installer->setUpdate(true); | |
374 } | |
375 | |
376 $installer->run(); | |
377 } | |
378 // @codeCoverageIgnoreEnd | |
379 } | |
380 } | |
381 // vim:sw=4:ts=4:sts=4:et: |