Update to use composer root object

Move shared commands to trait
This commit is contained in:
Damian Mooyman 2017-07-10 12:10:18 +12:00
parent d1d1b59662
commit fb72c0fcc4
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
2 changed files with 204 additions and 167 deletions

View File

@ -0,0 +1,185 @@
<?php
namespace SilverStripe\RecipePlugin;
use BadMethodCallException;
use Composer\Command\RequireCommand;
use Composer\Command\UpdateCommand;
use Composer\Composer;
use MongoDB\Driver\Exception\InvalidArgumentException;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\OutputInterface;
trait RecipeCommandBehaviour
{
/**
* Gets the application instance for this command.
*
* @return Application An Application instance
*/
public abstract function getApplication();
/**
* @param bool $required
* @param bool|null $disablePlugins
* @throws \RuntimeException
* @return Composer
*/
public abstract function getComposer($required = true, $disablePlugins = null);
/**
* Removes the cached composer instance
*/
public abstract function resetComposer();
/**
* Load composer data from the given directory
*
* @param string $path
* @param array|null $default If file doesn't exist use this default. If null, file is mandatory and there is
* no default
* @return array
*/
protected function loadComposer($path, $default = null)
{
if (!file_exists($path)) {
if (isset($default)) {
return $default;
}
throw new BadMethodCallException("Could not find " . basename($path));
}
$data = json_decode(file_get_contents($path), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \LogicException("Invalid composer.json with error: " . json_last_error_msg());
}
return $data;
}
/**
* Save the given data to the composer file in the given directory
*
* @param string $directory
* @param array $data
*/
protected function saveComposer($directory, $data)
{
$path = $directory.'/composer.json';
if (!file_exists($path)) {
throw new BadMethodCallException("Could not find composer.json");
}
$content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Make sure errors are reported
if (json_last_error()) {
throw new InvalidArgumentException("Invalid composer.json data with error: " . json_last_error_msg());
}
file_put_contents($path, $content);
}
/**
* @param OutputInterface $output
* @param string $recipe
* @param string $constraint
* @return int
*/
protected function requireRecipe(OutputInterface $output, $recipe, $constraint = null)
{
/** @var RequireCommand $command */
$command = $this->getApplication()->find('require');
$packages = [$recipe];
if ($constraint) {
$packages[] = $constraint;
}
$arguments = [
'command' => 'require',
'packages' => $packages,
];
$requireInput = new ArrayInput($arguments);
$returnCode = $command->run($requireInput, $output);
// Flush modified composer object
$this->resetComposer();
return $returnCode;
}
/**
* Update the project
*
* @param OutputInterface $output
* @return int
*/
protected function updateProject(OutputInterface $output)
{
/** @var UpdateCommand $command */
$command = $this->getApplication()->find('update');
$arguments = [ 'command' => 'update' ];
$requireInput = new ArrayInput($arguments);
$returnCode = $command->run($requireInput, $output);
// Flush modified composer object
$this->resetComposer();
return $returnCode;
}
/**
* Find installed version or constraint
*
* @param string $recipe
* @return string
*/
protected function findInstalledVersion($recipe)
{
// Check locker
$installed = $this->getComposer()->getLocker()->getLockedRepository()->findPackage($recipe, '*');
if ($installed) {
return $installed->getPrettyVersion();
}
// Check provides
$provides = $this->getComposer()->getPackage()->getProvides();
if (isset($provides[$recipe])) {
return $provides[$recipe]->getPrettyConstraint();
}
// Check requires
$requires = $this->getComposer()->getPackage()->getRequires();
if (isset($requires[$recipe])) {
return $provides[$recipe]->getPrettyConstraint();
}
// No existing version
return null;
}
/**
* Guess constraint to use if not provided
*
* @param string $existingVersion Known installed version
* @return string
*/
protected function findBestConstraint($existingVersion)
{
// Cannot guess without existing version
if (!$existingVersion) {
return null;
}
// Existing version is already a ^1.0.0 or ~1.0.0 constraint
if (preg_match('#^[~^]#', $existingVersion)) {
return $existingVersion;
}
// Existing version is already a dev constraint
if (stristr($existingVersion, 'dev') !== false) {
return $existingVersion;
}
// Numeric-only version maps to semver constraint
if (preg_match('#^([\d.]+)$#', $existingVersion)) {
return "^{$existingVersion}";
}
// Cannot guess; Let composer choose (equivalent to `composer require vendor/library`)
return null;
}
}

View File

@ -2,18 +2,19 @@
namespace SilverStripe\RecipePlugin; namespace SilverStripe\RecipePlugin;
use BadMethodCallException;
use Composer\Command\BaseCommand; use Composer\Command\BaseCommand;
use Composer\Command\RequireCommand;
use Composer\Command\UpdateCommand;
use MongoDB\Driver\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/**
* Provides the 'require-recipe' command which allows a new recipe to be installed, but also
* soft-updates any existing recipe.
*/
class RequireRecipeCommand extends BaseCommand class RequireRecipeCommand extends BaseCommand
{ {
use RecipeCommandBehaviour;
public function configure() public function configure()
{ {
$this->setName('require-recipe'); $this->setName('require-recipe');
@ -52,14 +53,11 @@ HELP
public function execute(InputInterface $input, OutputInterface $output) public function execute(InputInterface $input, OutputInterface $output)
{ {
// Get args and existing composer data
$recipe = $input->getArgument('recipe'); $recipe = $input->getArgument('recipe');
$constraint = $input->getArgument('version'); $constraint = $input->getArgument('version');
// Check if this is already installed // Check if this is already installed and notify users
$installedVersion = $this->findInstalledVersion($recipe); $installedVersion = $this->findInstalledVersion($recipe);
// Notify users of which version is being updated
if ($installedVersion) { if ($installedVersion) {
if ($constraint) { if ($constraint) {
$output->writeln( $output->writeln(
@ -87,18 +85,23 @@ HELP
return $returnCode; return $returnCode;
} }
// Get composer data for both root and newly installed recipe // Begin modification of composer.json
$composerData = $this->loadComposer(getcwd() .'/composer.json'); $composerData = $this->loadComposer(getcwd() .'/composer.json');
$recipeData = $this->loadComposer(getcwd().'/vendor/'.$recipe.'/composer.json');
// Promote all dependencies // Get composer data for both root and newly installed recipe
if (!empty($recipeData['require'])) { $installedRecipe = $this
->getComposer()
->getRepositoryManager()
->getLocalRepository()
->findPackage($recipe, '*');
if ($installedRecipe) {
$output->writeln("Inlining all dependencies for recipe <info>{$recipe}</info>:"); $output->writeln("Inlining all dependencies for recipe <info>{$recipe}</info>:");
foreach ($recipeData['require'] as $dependencyName => $dependencyVersion) { foreach ($installedRecipe->getRequires() as $requireName => $require) {
$requireVersion = $require->getPrettyConstraint();
$output->writeln( $output->writeln(
" * Inline dependency <info>{$dependencyName}</info> as <info>{$dependencyVersion}</info>" " * Inline dependency <info>{$requireName}</info> as <info>{$requireVersion}</info>"
); );
$composerData['require'][$dependencyName] = $dependencyVersion; $composerData['require'][$requireName] = $requireVersion;
} }
} }
@ -114,155 +117,4 @@ HELP
$this->saveComposer(getcwd(), $composerData); $this->saveComposer(getcwd(), $composerData);
return $this->updateProject($output); return $this->updateProject($output);
} }
/**
* Load composer data from the given directory
*
* @param string $path
* @param array|null $default If file doesn't exist use this default. If null, file is mandatory and there is
* no default
* @return array
*/
protected function loadComposer($path, $default = null)
{
if (!file_exists($path)) {
if (isset($default)) {
return $default;
}
throw new BadMethodCallException("Could not find " . basename($path));
}
$data = json_decode(file_get_contents($path), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \LogicException("Invalid composer.json with error: " . json_last_error_msg());
}
return $data;
}
/**
* Save the given data to the composer file in the given directory
*
* @param string $directory
* @param array $data
*/
protected function saveComposer($directory, $data)
{
$path = $directory.'/composer.json';
if (!file_exists($path)) {
throw new BadMethodCallException("Could not find composer.json");
}
$content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Make sure errors are reported
if (json_last_error()) {
throw new InvalidArgumentException("Invalid composer.json data with error: " . json_last_error_msg());
}
file_put_contents($path, $content);
}
/**
* @param OutputInterface $output
* @param string $recipe
* @param string $constraint
* @return int
*/
protected function requireRecipe(OutputInterface $output, $recipe, $constraint = null)
{
/** @var RequireCommand $command */
$command = $this->getApplication()->find('require');
$packages = [$recipe];
if ($constraint) {
$packages[] = $constraint;
}
$arguments = [
'command' => 'require',
'packages' => $packages,
];
$requireInput = new ArrayInput($arguments);
$returnCode = $command->run($requireInput, $output);
return $returnCode;
}
/**
* Update the project
*
* @param OutputInterface $output
* @return int
*/
protected function updateProject(OutputInterface $output)
{
/** @var UpdateCommand $command */
$command = $this->getApplication()->find('update');
$arguments = [ 'command' => 'update' ];
$requireInput = new ArrayInput($arguments);
$returnCode = $command->run($requireInput, $output);
return $returnCode;
}
/**
* @param string $recipe
* @return string
*/
protected function findInstalledVersion($recipe)
{
// Check composer.lock file
$lockData = $this->loadComposer(getcwd() . '/composer.lock', []);
if (isset($lockData['packages'])) {
foreach ($lockData['packages'] as $package) {
// Get version of installed file
if (isset($package['name']) && isset($package['version']) && $package['name'] === $recipe) {
$version = $package['version'];
// Trim leading `v` from `v1.0.0`
if (preg_match('#v([\d.]+)#i', $version)) {
return substr($version, 1);
}
return $version;
}
}
}
// Check composer.json
$composerData = $this->loadComposer(getcwd() . '/composer.json');
// Check provide for previously inlined recipe
if (isset($composerData['provide'][$recipe])) {
return $composerData['provide'][$recipe];
}
// Check existing constraints, or installed version
if (isset($composerData['require'][$recipe])) {
return $composerData['require'][$recipe];
}
return null;
}
/**
* Guess constraint to use if not provided
*
* @param string $existingVersion Known installed version
* @return string
*/
protected function findBestConstraint($existingVersion)
{
// Cannot guess without existing version
if (!$existingVersion) {
return null;
}
// Existing version is already a ^1.0.0 or ~1.0.0 constraint
if (preg_match('#^[~^]#', $existingVersion)) {
return $existingVersion;
}
// Existing version is already a dev constraint
if (stristr($existingVersion, 'dev') !== false) {
return $existingVersion;
}
// Numeric-only version maps to semver constraint
if (preg_match('#^([\d.]+)$#', $existingVersion)) {
return "^{$existingVersion}";
}
// Cannot guess; Let composer choose (equivalent to `composer require vendor/library`)
return null;
}
} }