Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,102 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer;
use Composer\IO\IOInterface;
/**
* Simple logging wrapper for Composer\IO\IOInterface
*
* @author Bryan Davis <bd808@bd808.com>
*/
class Logger
{
/**
* @var string $name
*/
protected $name;
/**
* @var IOInterface $inputOutput
*/
protected $inputOutput;
/**
* @param string $name
* @param IOInterface $io
*/
public function __construct($name, IOInterface $io)
{
$this->name = $name;
$this->inputOutput = $io;
}
/**
* Log a debug message
*
* Messages will be output at the "very verbose" logging level (eg `-vv`
* needed on the Composer command).
*
* @param string $message
*/
public function debug($message)
{
if ($this->inputOutput->isVeryVerbose()) {
$message = " <info>[{$this->name}]</info> {$message}";
$this->log($message);
}
}
/**
* Log an informative message
*
* Messages will be output at the "verbose" logging level (eg `-v` needed
* on the Composer command).
*
* @param string $message
*/
public function info($message)
{
if ($this->inputOutput->isVerbose()) {
$message = " <info>[{$this->name}]</info> {$message}";
$this->log($message);
}
}
/**
* Log a warning message
*
* @param string $message
*/
public function warning($message)
{
$message = " <error>[{$this->name}]</error> {$message}";
$this->log($message);
}
/**
* Write a message
*
* @param string $message
*/
protected function log($message)
{
if (method_exists($this->inputOutput, 'writeError')) {
$this->inputOutput->writeError($message);
} else {
// @codeCoverageIgnoreStart
// Backwards compatiblity for Composer before cb336a5
$this->inputOutput->write($message);
// @codeCoverageIgnoreEnd
}
}
}
// vim:sw=4:ts=4:sts=4:et:

View file

@ -0,0 +1,507 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge;
use Wikimedia\Composer\Logger;
use Composer\Composer;
use Composer\Json\JsonFile;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackage;
use Composer\Package\Link;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\RootAliasPackage;
use Composer\Package\RootPackage;
use Composer\Package\RootPackageInterface;
use Composer\Package\Version\VersionParser;
use UnexpectedValueException;
/**
* Processing for a composer.json file that will be merged into
* a RootPackageInterface
*
* @author Bryan Davis <bd808@bd808.com>
*/
class ExtraPackage
{
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var Logger $logger
*/
protected $logger;
/**
* @var string $path
*/
protected $path;
/**
* @var array $json
*/
protected $json;
/**
* @var CompletePackage $package
*/
protected $package;
/**
* @param string $path Path to composer.json file
* @param Composer $composer
* @param Logger $logger
*/
public function __construct($path, Composer $composer, Logger $logger)
{
$this->path = $path;
$this->composer = $composer;
$this->logger = $logger;
$this->json = $this->readPackageJson($path);
$this->package = $this->loadPackage($this->json);
}
/**
* Get list of additional packages to include if precessing recursively.
*
* @return array
*/
public function getIncludes()
{
return isset($this->json['extra']['merge-plugin']['include']) ?
$this->json['extra']['merge-plugin']['include'] : array();
}
/**
* Get list of additional packages to require if precessing recursively.
*
* @return array
*/
public function getRequires()
{
return isset($this->json['extra']['merge-plugin']['require']) ?
$this->json['extra']['merge-plugin']['require'] : array();
}
/**
* Read the contents of a composer.json style file into an array.
*
* The package contents are fixed up to be usable to create a Package
* object by providing dummy "name" and "version" values if they have not
* been provided in the file. This is consistent with the default root
* package loading behavior of Composer.
*
* @param string $path
* @return array
*/
protected function readPackageJson($path)
{
$file = new JsonFile($path);
$json = $file->read();
if (!isset($json['name'])) {
$json['name'] = 'merge-plugin/' .
strtr($path, DIRECTORY_SEPARATOR, '-');
}
if (!isset($json['version'])) {
$json['version'] = '1.0.0';
}
return $json;
}
/**
* @param string $json
* @return CompletePackage
*/
protected function loadPackage($json)
{
$loader = new ArrayLoader();
$package = $loader->load($json);
// @codeCoverageIgnoreStart
if (!$package instanceof CompletePackage) {
throw new UnexpectedValueException(
'Expected instance of CompletePackage, got ' .
get_class($package)
);
}
// @codeCoverageIgnoreEnd
return $package;
}
/**
* Merge this package into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeInto(RootPackageInterface $root, PluginState $state)
{
$this->addRepositories($root);
$this->mergeRequires('require', $root, $state);
if ($state->isDevMode()) {
$this->mergeRequires('require-dev', $root, $state);
}
$this->mergePackageLinks('conflict', $root);
$this->mergePackageLinks('replace', $root);
$this->mergePackageLinks('provide', $root);
$this->mergeSuggests($root);
$this->mergeAutoload('autoload', $root);
if ($state->isDevMode()) {
$this->mergeAutoload('devAutoload', $root);
}
$this->mergeExtra($root, $state);
}
/**
* Add a collection of repositories described by the given configuration
* to the given package and the global repository manager.
*
* @param RootPackageInterface $root
*/
protected function addRepositories(RootPackageInterface $root)
{
if (!isset($this->json['repositories'])) {
return;
}
$repoManager = $this->composer->getRepositoryManager();
$newRepos = array();
foreach ($this->json['repositories'] as $repoJson) {
if (!isset($repoJson['type'])) {
continue;
}
$this->logger->info("Adding {$repoJson['type']} repository");
$repo = $repoManager->createRepository(
$repoJson['type'],
$repoJson
);
$repoManager->addRepository($repo);
$newRepos[] = $repo;
}
$unwrapped = self::unwrapIfNeeded($root, 'setRepositories');
$unwrapped->setRepositories(array_merge(
$newRepos,
$root->getRepositories()
));
}
/**
* Merge require or require-dev into a RootPackageInterface
*
* @param string $type 'require' or 'require-dev'
* @param RootPackageInterface $root
* @param PluginState $state
*/
protected function mergeRequires(
$type,
RootPackageInterface $root,
PluginState $state
) {
$linkType = BasePackage::$supportedLinkTypes[$type];
$getter = 'get' . ucfirst($linkType['method']);
$setter = 'set' . ucfirst($linkType['method']);
$requires = $this->package->{$getter}();
if (empty($requires)) {
return;
}
$this->mergeStabilityFlags($root, $requires);
$requires = $this->replaceSelfVersionDependencies(
$type,
$requires,
$root
);
$root->{$setter}($this->mergeOrDefer(
$type,
$root->{$getter}(),
$requires,
$state
));
}
/**
* Merge two collections of package links and collect duplicates for
* subsequent processing.
*
* @param string $type 'require' or 'require-dev'
* @param array $origin Primary collection
* @param array $merge Additional collection
* @param PluginState $state
* @return array Merged collection
*/
protected function mergeOrDefer(
$type,
array $origin,
array $merge,
$state
) {
$dups = array();
foreach ($merge as $name => $link) {
if (!isset($origin[$name]) || $state->replaceDuplicateLinks()) {
$this->logger->info("Merging <comment>{$name}</comment>");
$origin[$name] = $link;
} else {
// Defer to solver.
$this->logger->info(
"Deferring duplicate <comment>{$name}</comment>"
);
$dups[] = $link;
}
}
$state->addDuplicateLinks($type, $dups);
return $origin;
}
/**
* Merge autoload or autoload-dev into a RootPackageInterface
*
* @param string $type 'autoload' or 'devAutoload'
* @param RootPackageInterface $root
*/
protected function mergeAutoload($type, RootPackageInterface $root)
{
$getter = 'get' . ucfirst($type);
$setter = 'set' . ucfirst($type);
$autoload = $this->package->{$getter}();
if (empty($autoload)) {
return;
}
$unwrapped = self::unwrapIfNeeded($root, $setter);
$unwrapped->{$setter}(array_merge_recursive(
$root->{$getter}(),
$this->fixRelativePaths($autoload)
));
}
/**
* Fix a collection of paths that are relative to this package to be
* relative to the base package.
*
* @param array $paths
* @return array
*/
protected function fixRelativePaths(array $paths)
{
$base = dirname($this->path);
$base = ($base === '.') ? '' : "{$base}/";
array_walk_recursive(
$paths,
function (&$path) use ($base) {
$path = "{$base}{$path}";
}
);
return $paths;
}
/**
* Extract and merge stability flags from the given collection of
* requires and merge them into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param array $requires
*/
protected function mergeStabilityFlags(
RootPackageInterface $root,
array $requires
) {
$flags = $root->getStabilityFlags();
$sf = new StabilityFlags($flags, $root->getMinimumStability());
$unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags');
$unwrapped->setStabilityFlags(array_merge(
$flags,
$sf->extractAll($requires)
));
}
/**
* Merge package links of the given type into a RootPackageInterface
*
* @param string $type 'conflict', 'replace' or 'provide'
* @param RootPackageInterface $root
*/
protected function mergePackageLinks($type, RootPackageInterface $root)
{
$linkType = BasePackage::$supportedLinkTypes[$type];
$getter = 'get' . ucfirst($linkType['method']);
$setter = 'set' . ucfirst($linkType['method']);
$links = $this->package->{$getter}();
if (!empty($links)) {
$unwrapped = self::unwrapIfNeeded($root, $setter);
// @codeCoverageIgnoreStart
if ($root !== $unwrapped) {
$this->logger->warning(
'This Composer version does not support ' .
"'{$type}' merging for aliased packages."
);
}
// @codeCoverageIgnoreEnd
$unwrapped->{$setter}(array_merge(
$root->{$getter}(),
$this->replaceSelfVersionDependencies($type, $links, $root)
));
}
}
/**
* Merge suggested packages into a RootPackageInterface
*
* @param RootPackageInterface $root
*/
protected function mergeSuggests(RootPackageInterface $root)
{
$suggests = $this->package->getSuggests();
if (!empty($suggests)) {
$unwrapped = self::unwrapIfNeeded($root, 'setSuggests');
$unwrapped->setSuggests(array_merge(
$root->getSuggests(),
$suggests
));
}
}
/**
* Merge extra config into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeExtra(RootPackageInterface $root, PluginState $state)
{
$extra = $this->package->getExtra();
unset($extra['merge-plugin']);
if (!$state->shouldMergeExtra() || empty($extra)) {
return;
}
$rootExtra = $root->getExtra();
$unwrapped = self::unwrapIfNeeded($root, 'setExtra');
if ($state->replaceDuplicateLinks()) {
$unwrapped->setExtra(
array_merge($rootExtra, $extra)
);
} else {
foreach (array_intersect(
array_keys($extra),
array_keys($rootExtra)
) as $key) {
$this->logger->info(
"Ignoring duplicate <comment>{$key}</comment> in ".
"<comment>{$this->path}</comment> extra config."
);
}
$unwrapped->setExtra(
array_merge($extra, $rootExtra)
);
}
}
/**
* Update Links with a 'self.version' constraint with the root package's
* version.
*
* @param string $type Link type
* @param array $links
* @param RootPackageInterface $root
* @return array
*/
protected function replaceSelfVersionDependencies(
$type,
array $links,
RootPackageInterface $root
) {
$linkType = BasePackage::$supportedLinkTypes[$type];
$version = $root->getVersion();
$prettyVersion = $root->getPrettyVersion();
$vp = new VersionParser();
$method = 'get' . ucfirst($linkType['method']);
$packages = $root->$method();
return array_map(
function ($link) use ($linkType, $version, $prettyVersion, $vp, $packages) {
if ('self.version' === $link->getPrettyConstraint()) {
if (isset($packages[$link->getSource()])) {
/** @var Link $package */
$package = $packages[$link->getSource()];
return new Link(
$link->getSource(),
$link->getTarget(),
$vp->parseConstraints($package->getConstraint()->getPrettyString()),
$linkType['description'],
$package->getPrettyConstraint()
);
}
return new Link(
$link->getSource(),
$link->getTarget(),
$vp->parseConstraints($version),
$linkType['description'],
$prettyVersion
);
}
return $link;
},
$links
);
}
/**
* Get a full featured Package from a RootPackageInterface.
*
* In Composer versions before 599ad77 the RootPackageInterface only
* defines a sub-set of operations needed by composer-merge-plugin and
* RootAliasPackage only implemented those methods defined by the
* interface. Most of the unimplemented methods in RootAliasPackage can be
* worked around because the getter methods that are implemented proxy to
* the aliased package which we can modify by unwrapping. The exception
* being modifying the 'conflicts', 'provides' and 'replaces' collections.
* We have no way to actually modify those collections unfortunately in
* older versions of Composer.
*
* @param RootPackageInterface $root
* @param string $method Method needed
* @return RootPackageInterface|RootPackage
*/
public static function unwrapIfNeeded(
RootPackageInterface $root,
$method = 'setExtra'
) {
// @codeCoverageIgnoreStart
if ($root instanceof RootAliasPackage &&
!method_exists($root, $method)
) {
// Unwrap and return the aliased RootPackage.
$root = $root->getAliasOf();
}
// @codeCoverageIgnoreEnd
return $root;
}
}
// vim:sw=4:ts=4:sts=4:et:

View file

@ -0,0 +1,18 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge;
/**
* @author Bryan Davis <bd808@bd808.com>
*/
class MissingFileException extends \RuntimeException
{
}

View file

@ -0,0 +1,327 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge;
use Composer\Composer;
/**
* Mutable plugin state
*
* @author Bryan Davis <bd808@bd808.com>
*/
class PluginState
{
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var array $includes
*/
protected $includes = array();
/**
* @var array $requires
*/
protected $requires = array();
/**
* @var array $duplicateLinks
*/
protected $duplicateLinks = array();
/**
* @var bool $devMode
*/
protected $devMode = false;
/**
* @var bool $recurse
*/
protected $recurse = true;
/**
* @var bool $replace
*/
protected $replace = false;
/**
* Whether to merge the -dev sections.
* @var bool $mergeDev
*/
protected $mergeDev = true;
/**
* Whether to merge the extra section.
*
* By default, the extra section is not merged and there will be many
* cases where the merge of the extra section is performed too late
* to be of use to other plugins. When enabled, merging uses one of
* two strategies - either 'first wins' or 'last wins'. When enabled,
* 'first wins' is the default behaviour. If Replace mode is activated
* then 'last wins' is used.
*
* @var bool $mergeExtra
*/
protected $mergeExtra = false;
/**
* @var bool $firstInstall
*/
protected $firstInstall = false;
/**
* @var bool $locked
*/
protected $locked = false;
/**
* @var bool $dumpAutoloader
*/
protected $dumpAutoloader = false;
/**
* @var bool $optimizeAutoloader
*/
protected $optimizeAutoloader = false;
/**
* @param Composer $composer
*/
public function __construct(Composer $composer)
{
$this->composer = $composer;
}
/**
* Load plugin settings
*/
public function loadSettings()
{
$extra = $this->composer->getPackage()->getExtra();
$config = array_merge(
array(
'include' => array(),
'require' => array(),
'recurse' => true,
'replace' => false,
'merge-dev' => true,
'merge-extra' => false,
),
isset($extra['merge-plugin']) ? $extra['merge-plugin'] : array()
);
$this->includes = (is_array($config['include'])) ?
$config['include'] : array($config['include']);
$this->requires = (is_array($config['require'])) ?
$config['require'] : array($config['require']);
$this->recurse = (bool)$config['recurse'];
$this->replace = (bool)$config['replace'];
$this->mergeDev = (bool)$config['merge-dev'];
$this->mergeExtra = (bool)$config['merge-extra'];
}
/**
* Get list of filenames and/or glob patterns to include
*
* @return array
*/
public function getIncludes()
{
return $this->includes;
}
/**
* Get list of filenames and/or glob patterns to require
*
* @return array
*/
public function getRequires()
{
return $this->requires;
}
/**
* Set the first install flag
*
* @param bool $flag
*/
public function setFirstInstall($flag)
{
$this->firstInstall = (bool)$flag;
}
/**
* Is this the first time that the plugin has been installed?
*
* @return bool
*/
public function isFirstInstall()
{
return $this->firstInstall;
}
/**
* Set the locked flag
*
* @param bool $flag
*/
public function setLocked($flag)
{
$this->locked = (bool)$flag;
}
/**
* Was a lockfile present when the plugin was installed?
*
* @return bool
*/
public function isLocked()
{
return $this->locked;
}
/**
* Should an update be forced?
*
* @return true If packages are not locked
*/
public function forceUpdate()
{
return !$this->locked;
}
/**
* Set the devMode flag
*
* @param bool $flag
*/
public function setDevMode($flag)
{
$this->devMode = (bool)$flag;
}
/**
* Should devMode settings be processed?
*
* @return bool
*/
public function isDevMode()
{
return $this->mergeDev && $this->devMode;
}
/**
* Set the dumpAutoloader flag
*
* @param bool $flag
*/
public function setDumpAutoloader($flag)
{
$this->dumpAutoloader = (bool)$flag;
}
/**
* Is the autoloader file supposed to be written out?
*
* @return bool
*/
public function shouldDumpAutoloader()
{
return $this->dumpAutoloader;
}
/**
* Set the optimizeAutoloader flag
*
* @param bool $flag
*/
public function setOptimizeAutoloader($flag)
{
$this->optimizeAutoloader = (bool)$flag;
}
/**
* Should the autoloader be optimized?
*
* @return bool
*/
public function shouldOptimizeAutoloader()
{
return $this->optimizeAutoloader;
}
/**
* Add duplicate packages
*
* @param string $type Package type
* @param array $packages
*/
public function addDuplicateLinks($type, array $packages)
{
if (!isset($this->duplicateLinks[$type])) {
$this->duplicateLinks[$type] = array();
}
$this->duplicateLinks[$type] =
array_merge($this->duplicateLinks[$type], $packages);
}
/**
* Get duplicate packages
*
* @param string $type Package type
* @return array
*/
public function getDuplicateLinks($type)
{
return isset($this->duplicateLinks[$type]) ?
$this->duplicateLinks[$type] : array();
}
/**
* Should includes be recursively processed?
*
* @return bool
*/
public function recurseIncludes()
{
return $this->recurse;
}
/**
* Should duplicate links be replaced in a 'last definition wins' order?
*
* @return bool
*/
public function replaceDuplicateLinks()
{
return $this->replace;
}
/**
* Should the extra section be merged?
*
* By default, the extra section is not merged and there will be many
* cases where the merge of the extra section is performed too late
* to be of use to other plugins. When enabled, merging uses one of
* two strategies - either 'first wins' or 'last wins'. When enabled,
* 'first wins' is the default behaviour. If Replace mode is activated
* then 'last wins' is used.
*
* @return bool
*/
public function shouldMergeExtra()
{
return $this->mergeExtra;
}
}
// vim:sw=4:ts=4:sts=4:et:

View file

@ -0,0 +1,181 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge;
use Composer\Package\BasePackage;
use Composer\Package\Version\VersionParser;
/**
* Adapted from Composer's RootPackageLoader::extractStabilityFlags
* @author Bryan Davis <bd808@bd808.com>
*/
class StabilityFlags
{
/**
* @var array Current package name => stability mappings
*/
protected $stabilityFlags;
/**
* @var int Current default minimum stability
*/
protected $minimumStability;
/**
* @var string Regex to extract an explict stability flag (eg '@dev')
*/
protected $explicitStabilityRe;
/**
* @param array $stabilityFlags Current package name => stability mappings
* @param int $minimumStability Current default minimum stability
*/
public function __construct(
array $stabilityFlags = array(),
$minimumStability = BasePackage::STABILITY_STABLE
) {
$this->stabilityFlags = $stabilityFlags;
$this->minimumStability = $this->getStabilityInt($minimumStability);
$this->explicitStabilityRe = '/^[^@]*?@(' .
implode('|', array_keys(BasePackage::$stabilities)) .
')$/i';
}
/**
* Get the stability value for a given string.
*
* @param string $name Stability name
* @return int Stability value
*/
protected function getStabilityInt($name)
{
$name = VersionParser::normalizeStability($name);
return isset(BasePackage::$stabilities[$name]) ?
BasePackage::$stabilities[$name] :
BasePackage::STABILITY_STABLE;
}
/**
* Extract and merge stability flags from the given collection of
* requires with another collection of stability flags.
*
* @param array $requires New package name => link mappings
* @return array Unified package name => stability mappings
*/
public function extractAll(array $requires)
{
$flags = array();
foreach ($requires as $name => $link) {
$name = strtolower($name);
$version = $link->getPrettyConstraint();
$stability = $this->getExplicitStability($version);
if ($stability === null) {
$stability = $this->getParsedStability($version);
}
$flags[$name] = max($stability, $this->getCurrentStability($name));
}
// Filter out null stability values
return array_filter($flags, function ($v) {
return $v !== null;
});
}
/**
* Extract the most unstable explicit stability (eg '@dev') from a version
* specification.
*
* @param string $version
* @return int|null Stability or null if no explict stability found
*/
protected function getExplicitStability($version)
{
$found = null;
$constraints = $this->splitConstraints($version);
foreach ($constraints as $constraint) {
if (preg_match($this->explicitStabilityRe, $constraint, $match)) {
$stability = $this->getStabilityInt($match[1]);
$found = max($stability, $found);
}
}
return $found;
}
/**
* Split a version specification into a list of version constraints.
*
* @param string $version
* @return array
*/
protected function splitConstraints($version)
{
$found = array();
$orConstraints = preg_split('/\s*\|\|?\s*/', trim($version));
foreach ($orConstraints as $constraints) {
$andConstraints = preg_split(
'/(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)/',
$constraints
);
foreach ($andConstraints as $constraint) {
$found[] = $constraint;
}
}
return $found;
}
/**
* Get the stability of a version
*
* @param string $version
* @return int|null Stability or null if STABLE or less than minimum
*/
protected function getParsedStability($version)
{
// Drop aliasing if used
$version = preg_replace('/^([^,\s@]+) as .+$/', '$1', $version);
$stability = $this->getStabilityInt(
VersionParser::parseStability($version)
);
if ($stability === BasePackage::STABILITY_STABLE ||
$this->minimumStability > $stability
) {
// Ignore if 'stable' or more stable than the global
// minimum
$stability = null;
}
return $stability;
}
/**
* Get the current stability of a given package.
*
* @param string $name
* @return int|null Stability of null if not set
*/
protected function getCurrentStability($name)
{
return isset($this->stabilityFlags[$name]) ?
$this->stabilityFlags[$name] : null;
}
}
// vim:sw=4:ts=4:sts=4:et:

View file

@ -0,0 +1,314 @@
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer;
use Wikimedia\Composer\Merge\ExtraPackage;
use Wikimedia\Composer\Merge\MissingFileException;
use Wikimedia\Composer\Merge\PluginState;
use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Factory;
use Composer\Installer;
use Composer\Installer\InstallerEvent;
use Composer\Installer\InstallerEvents;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\IO\IOInterface;
use Composer\Package\RootPackageInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
/**
* Composer plugin that allows merging multiple composer.json files.
*
* When installed, this plugin will look for a "merge-plugin" key in the
* composer configuration's "extra" section. The value for this key is
* a set of options configuring the plugin.
*
* An "include" setting is required. The value of this setting can be either
* a single value or an array of values. Each value is treated as a glob()
* pattern identifying additional composer.json style configuration files to
* merge into the configuration for the current compser execution.
*
* The "autoload", "autoload-dev", "conflict", "provide", "replace",
* "repositories", "require", "require-dev", and "suggest" sections of the
* found configuration files will be merged into the root package
* configuration as though they were directly included in the top-level
* composer.json file.
*
* If included files specify conflicting package versions for "require" or
* "require-dev", the normal Composer dependency solver process will be used
* to attempt to resolve the conflict. Specifying the 'replace' key as true will
* change this default behaviour so that the last-defined version of a package
* will win, allowing for force-overrides of package defines.
*
* By default the "extra" section is not merged. This can be enabled by
* setitng the 'merge-extra' key to true. In normal mode, when the same key is
* found in both the original and the imported extra section, the version in
* the original config is used and the imported version is skipped. If
* 'replace' mode is active, this behaviour changes so the imported version of
* the key is used, replacing the version in the original config.
*
*
* @code
* {
* "require": {
* "wikimedia/composer-merge-plugin": "dev-master"
* },
* "extra": {
* "merge-plugin": {
* "include": [
* "composer.local.json"
* ]
* }
* }
* }
* @endcode
*
* @author Bryan Davis <bd808@bd808.com>
*/
class MergePlugin implements PluginInterface, EventSubscriberInterface
{
/**
* Offical package name
*/
const PACKAGE_NAME = 'wikimedia/composer-merge-plugin';
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var PluginState $state
*/
protected $state;
/**
* @var Logger $logger
*/
protected $logger;
/**
* Files that have already been processed
*
* @var string[] $loadedFiles
*/
protected $loadedFiles = array();
/**
* {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io)
{
$this->composer = $composer;
$this->state = new PluginState($this->composer);
$this->logger = new Logger('merge-plugin', $io);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
InstallerEvents::PRE_DEPENDENCIES_SOLVING => 'onDependencySolve',
PackageEvents::POST_PACKAGE_INSTALL => 'onPostPackageInstall',
ScriptEvents::POST_INSTALL_CMD => 'onPostInstallOrUpdate',
ScriptEvents::POST_UPDATE_CMD => 'onPostInstallOrUpdate',
ScriptEvents::PRE_AUTOLOAD_DUMP => 'onInstallUpdateOrDump',
ScriptEvents::PRE_INSTALL_CMD => 'onInstallUpdateOrDump',
ScriptEvents::PRE_UPDATE_CMD => 'onInstallUpdateOrDump',
);
}
/**
* Handle an event callback for an install, update or dump command by
* checking for "merge-plugin" in the "extra" data and merging package
* contents if found.
*
* @param Event $event
*/
public function onInstallUpdateOrDump(Event $event)
{
$this->state->loadSettings();
$this->state->setDevMode($event->isDevMode());
$this->mergeFiles($this->state->getIncludes(), false);
$this->mergeFiles($this->state->getRequires(), true);
if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
$this->state->setDumpAutoloader(true);
$flags = $event->getFlags();
if (isset($flags['optimize'])) {
$this->state->setOptimizeAutoloader($flags['optimize']);
}
}
}
/**
* Find configuration files matching the configured glob patterns and
* merge their contents with the master package.
*
* @param array $patterns List of files/glob patterns
* @param bool $required Are the patterns required to match files?
* @throws MissingFileException when required and a pattern returns no
* results
*/
protected function mergeFiles(array $patterns, $required = false)
{
$root = $this->composer->getPackage();
$files = array_map(
function ($files, $pattern) use ($required) {
if ($required && !$files) {
throw new MissingFileException(
"merge-plugin: No files matched required '{$pattern}'"
);
}
return $files;
},
array_map('glob', $patterns),
$patterns
);
foreach (array_reduce($files, 'array_merge', array()) as $path) {
$this->mergeFile($root, $path);
}
}
/**
* Read a JSON file and merge its contents
*
* @param RootPackageInterface $root
* @param string $path
*/
protected function mergeFile(RootPackageInterface $root, $path)
{
if (isset($this->loadedFiles[$path])) {
$this->logger->debug("Already merged <comment>$path</comment>");
return;
} else {
$this->loadedFiles[$path] = true;
}
$this->logger->info("Loading <comment>{$path}</comment>...");
$package = new ExtraPackage($path, $this->composer, $this->logger);
$package->mergeInto($root, $this->state);
if ($this->state->recurseIncludes()) {
$this->mergeFiles($package->getIncludes(), false);
$this->mergeFiles($package->getRequires(), true);
}
}
/**
* Handle an event callback for pre-dependency solving phase of an install
* or update by adding any duplicate package dependencies found during
* initial merge processing to the request that will be processed by the
* dependency solver.
*
* @param InstallerEvent $event
*/
public function onDependencySolve(InstallerEvent $event)
{
$request = $event->getRequest();
foreach ($this->state->getDuplicateLinks('require') as $link) {
$this->logger->info(
"Adding dependency <comment>{$link}</comment>"
);
$request->install($link->getTarget(), $link->getConstraint());
}
if ($this->state->isDevMode()) {
foreach ($this->state->getDuplicateLinks('require-dev') as $link) {
$this->logger->info(
"Adding dev dependency <comment>{$link}</comment>"
);
$request->install($link->getTarget(), $link->getConstraint());
}
}
}
/**
* Handle an event callback following installation of a new package by
* checking to see if the package that was installed was our plugin.
*
* @param PackageEvent $event
*/
public function onPostPackageInstall(PackageEvent $event)
{
$op = $event->getOperation();
if ($op instanceof InstallOperation) {
$package = $op->getPackage()->getName();
if ($package === self::PACKAGE_NAME) {
$this->logger->info('composer-merge-plugin installed');
$this->state->setFirstInstall(true);
$this->state->setLocked(
$event->getComposer()->getLocker()->isLocked()
);
}
}
}
/**
* Handle an event callback following an install or update command. If our
* plugin was installed during the run then trigger an update command to
* process any merge-patterns in the current config.
*
* @param Event $event
*/
public function onPostInstallOrUpdate(Event $event)
{
// @codeCoverageIgnoreStart
if ($this->state->isFirstInstall()) {
$this->state->setFirstInstall(false);
$this->logger->info(
'<comment>' .
'Running additional update to apply merge settings' .
'</comment>'
);
$config = $this->composer->getConfig();
$preferSource = $config->get('preferred-install') == 'source';
$preferDist = $config->get('preferred-install') == 'dist';
$installer = Installer::create(
$event->getIO(),
// Create a new Composer instance to ensure full processing of
// the merged files.
Factory::create($event->getIO(), null, false)
);
$installer->setPreferSource($preferSource);
$installer->setPreferDist($preferDist);
$installer->setDevMode($event->isDevMode());
$installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
$installer->setOptimizeAutoloader(
$this->state->shouldOptimizeAutoloader()
);
if ($this->state->forceUpdate()) {
// Force update mode so that new packages are processed rather
// than just telling the user that composer.json and
// composer.lock don't match.
$installer->setUpdate(true);
}
$installer->run();
}
// @codeCoverageIgnoreEnd
}
}
// vim:sw=4:ts=4:sts=4:et: