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: