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