Chris@0: Chris@0: */ Chris@0: class MergePlugin implements PluginInterface, EventSubscriberInterface Chris@0: { Chris@0: Chris@0: /** Chris@0: * Offical package name Chris@0: */ Chris@0: const PACKAGE_NAME = 'wikimedia/composer-merge-plugin'; Chris@0: Chris@0: /** Chris@0: * Name of the composer 1.1 init event. Chris@0: */ Chris@0: const COMPAT_PLUGINEVENTS_INIT = 'init'; Chris@0: Chris@0: /** Chris@0: * Priority that plugin uses to register callbacks. Chris@0: */ Chris@0: const CALLBACK_PRIORITY = 50000; Chris@0: Chris@0: /** Chris@0: * @var Composer $composer Chris@0: */ Chris@0: protected $composer; Chris@0: Chris@0: /** Chris@0: * @var PluginState $state Chris@0: */ Chris@0: protected $state; Chris@0: Chris@0: /** Chris@0: * @var Logger $logger Chris@0: */ Chris@0: protected $logger; Chris@0: Chris@0: /** Chris@0: * Files that have already been fully processed Chris@0: * Chris@0: * @var string[] $loaded Chris@0: */ Chris@0: protected $loaded = array(); Chris@0: Chris@0: /** Chris@0: * Files that have already been partially processed Chris@0: * Chris@0: * @var string[] $loadedNoDev Chris@0: */ Chris@0: protected $loadedNoDev = array(); Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function activate(Composer $composer, IOInterface $io) Chris@0: { Chris@0: $this->composer = $composer; Chris@0: $this->state = new PluginState($this->composer); Chris@0: $this->logger = new Logger('merge-plugin', $io); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function getSubscribedEvents() Chris@0: { Chris@0: return array( Chris@0: // Use our own constant to make this event optional. Once Chris@0: // composer-1.1 is required, this can use PluginEvents::INIT Chris@0: // instead. Chris@0: self::COMPAT_PLUGINEVENTS_INIT => Chris@0: array('onInit', self::CALLBACK_PRIORITY), Chris@0: InstallerEvents::PRE_DEPENDENCIES_SOLVING => Chris@0: array('onDependencySolve', self::CALLBACK_PRIORITY), Chris@0: PackageEvents::POST_PACKAGE_INSTALL => Chris@0: array('onPostPackageInstall', self::CALLBACK_PRIORITY), Chris@0: ScriptEvents::POST_INSTALL_CMD => Chris@0: array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY), Chris@0: ScriptEvents::POST_UPDATE_CMD => Chris@0: array('onPostInstallOrUpdate', self::CALLBACK_PRIORITY), Chris@0: ScriptEvents::PRE_AUTOLOAD_DUMP => Chris@0: array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), Chris@0: ScriptEvents::PRE_INSTALL_CMD => Chris@0: array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), Chris@0: ScriptEvents::PRE_UPDATE_CMD => Chris@0: array('onInstallUpdateOrDump', self::CALLBACK_PRIORITY), Chris@0: ); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handle an event callback for initialization. Chris@0: * Chris@0: * @param \Composer\EventDispatcher\Event $event Chris@0: */ Chris@0: public function onInit(BaseEvent $event) Chris@0: { Chris@0: $this->state->loadSettings(); Chris@0: // It is not possible to know if the user specified --dev or --no-dev Chris@0: // so assume it is false. The dev section will be merged later when Chris@0: // the other events fire. Chris@0: $this->state->setDevMode(false); Chris@0: $this->mergeFiles($this->state->getIncludes(), false); Chris@0: $this->mergeFiles($this->state->getRequires(), true); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handle an event callback for an install, update or dump command by Chris@0: * checking for "merge-plugin" in the "extra" data and merging package Chris@0: * contents if found. Chris@0: * Chris@0: * @param ScriptEvent $event Chris@0: */ Chris@0: public function onInstallUpdateOrDump(ScriptEvent $event) Chris@0: { Chris@0: $this->state->loadSettings(); Chris@0: $this->state->setDevMode($event->isDevMode()); Chris@0: $this->mergeFiles($this->state->getIncludes(), false); Chris@0: $this->mergeFiles($this->state->getRequires(), true); Chris@0: Chris@0: if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) { Chris@0: $this->state->setDumpAutoloader(true); Chris@0: $flags = $event->getFlags(); Chris@0: if (isset($flags['optimize'])) { Chris@0: $this->state->setOptimizeAutoloader($flags['optimize']); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Find configuration files matching the configured glob patterns and Chris@0: * merge their contents with the master package. Chris@0: * Chris@0: * @param array $patterns List of files/glob patterns Chris@0: * @param bool $required Are the patterns required to match files? Chris@0: * @throws MissingFileException when required and a pattern returns no Chris@0: * results Chris@0: */ Chris@0: protected function mergeFiles(array $patterns, $required = false) Chris@0: { Chris@0: $root = $this->composer->getPackage(); Chris@0: Chris@0: $files = array_map( Chris@0: function ($files, $pattern) use ($required) { Chris@0: if ($required && !$files) { Chris@0: throw new MissingFileException( Chris@0: "merge-plugin: No files matched required '{$pattern}'" Chris@0: ); Chris@0: } Chris@0: return $files; Chris@0: }, Chris@0: array_map('glob', $patterns), Chris@0: $patterns Chris@0: ); Chris@0: Chris@0: foreach (array_reduce($files, 'array_merge', array()) as $path) { Chris@0: $this->mergeFile($root, $path); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Read a JSON file and merge its contents Chris@0: * Chris@0: * @param RootPackageInterface $root Chris@0: * @param string $path Chris@0: */ Chris@0: protected function mergeFile(RootPackageInterface $root, $path) Chris@0: { Chris@0: if (isset($this->loaded[$path]) || Chris@0: (isset($this->loadedNoDev[$path]) && !$this->state->isDevMode()) Chris@0: ) { Chris@0: $this->logger->debug( Chris@0: "Already merged $path completely" Chris@0: ); Chris@0: return; Chris@0: } Chris@0: Chris@0: $package = new ExtraPackage($path, $this->composer, $this->logger); Chris@0: Chris@0: if (isset($this->loadedNoDev[$path])) { Chris@0: $this->logger->info( Chris@0: "Loading -dev sections of {$path}..." Chris@0: ); Chris@0: $package->mergeDevInto($root, $this->state); Chris@0: } else { Chris@0: $this->logger->info("Loading {$path}..."); Chris@0: $package->mergeInto($root, $this->state); Chris@0: } Chris@0: Chris@0: if ($this->state->isDevMode()) { Chris@0: $this->loaded[$path] = true; Chris@0: } else { Chris@0: $this->loadedNoDev[$path] = true; Chris@0: } Chris@0: Chris@0: if ($this->state->recurseIncludes()) { Chris@0: $this->mergeFiles($package->getIncludes(), false); Chris@0: $this->mergeFiles($package->getRequires(), true); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handle an event callback for pre-dependency solving phase of an install Chris@0: * or update by adding any duplicate package dependencies found during Chris@0: * initial merge processing to the request that will be processed by the Chris@0: * dependency solver. Chris@0: * Chris@0: * @param InstallerEvent $event Chris@0: */ Chris@0: public function onDependencySolve(InstallerEvent $event) Chris@0: { Chris@0: $request = $event->getRequest(); Chris@0: foreach ($this->state->getDuplicateLinks('require') as $link) { Chris@0: $this->logger->info( Chris@0: "Adding dependency {$link}" Chris@0: ); Chris@0: $request->install($link->getTarget(), $link->getConstraint()); Chris@0: } Chris@0: Chris@0: // Issue #113: Check devMode of event rather than our global state. Chris@0: // Composer fires the PRE_DEPENDENCIES_SOLVING event twice for Chris@0: // `--no-dev` operations to decide which packages are dev only Chris@0: // requirements. Chris@0: if ($this->state->shouldMergeDev() && $event->isDevMode()) { Chris@0: foreach ($this->state->getDuplicateLinks('require-dev') as $link) { Chris@0: $this->logger->info( Chris@0: "Adding dev dependency {$link}" Chris@0: ); Chris@0: $request->install($link->getTarget(), $link->getConstraint()); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handle an event callback following installation of a new package by Chris@0: * checking to see if the package that was installed was our plugin. Chris@0: * Chris@0: * @param PackageEvent $event Chris@0: */ Chris@0: public function onPostPackageInstall(PackageEvent $event) Chris@0: { Chris@0: $op = $event->getOperation(); Chris@0: if ($op instanceof InstallOperation) { Chris@0: $package = $op->getPackage()->getName(); Chris@0: if ($package === self::PACKAGE_NAME) { Chris@0: $this->logger->info('composer-merge-plugin installed'); Chris@0: $this->state->setFirstInstall(true); Chris@0: $this->state->setLocked( Chris@0: $event->getComposer()->getLocker()->isLocked() Chris@0: ); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handle an event callback following an install or update command. If our Chris@0: * plugin was installed during the run then trigger an update command to Chris@0: * process any merge-patterns in the current config. Chris@0: * Chris@0: * @param ScriptEvent $event Chris@0: */ Chris@0: public function onPostInstallOrUpdate(ScriptEvent $event) Chris@0: { Chris@0: // @codeCoverageIgnoreStart Chris@0: if ($this->state->isFirstInstall()) { Chris@0: $this->state->setFirstInstall(false); Chris@0: $this->logger->info( Chris@0: '' . Chris@0: 'Running additional update to apply merge settings' . Chris@0: '' Chris@0: ); Chris@0: Chris@0: $config = $this->composer->getConfig(); Chris@0: Chris@0: $preferSource = $config->get('preferred-install') == 'source'; Chris@0: $preferDist = $config->get('preferred-install') == 'dist'; Chris@0: Chris@0: $installer = Installer::create( Chris@0: $event->getIO(), Chris@0: // Create a new Composer instance to ensure full processing of Chris@0: // the merged files. Chris@0: Factory::create($event->getIO(), null, false) Chris@0: ); Chris@0: Chris@0: $installer->setPreferSource($preferSource); Chris@0: $installer->setPreferDist($preferDist); Chris@0: $installer->setDevMode($event->isDevMode()); Chris@0: $installer->setDumpAutoloader($this->state->shouldDumpAutoloader()); Chris@0: $installer->setOptimizeAutoloader( Chris@0: $this->state->shouldOptimizeAutoloader() Chris@0: ); Chris@0: Chris@0: if ($this->state->forceUpdate()) { Chris@0: // Force update mode so that new packages are processed rather Chris@0: // than just telling the user that composer.json and Chris@0: // composer.lock don't match. Chris@0: $installer->setUpdate(true); Chris@0: } Chris@0: Chris@0: $installer->run(); Chris@0: } Chris@0: // @codeCoverageIgnoreEnd Chris@0: } Chris@0: } Chris@0: // vim:sw=4:ts=4:sts=4:et: