]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - vendor/wikimedia/composer-merge-plugin/src/MergePlugin.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / vendor / wikimedia / composer-merge-plugin / src / MergePlugin.php
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: