From 54857a20a3d5f30534888d772be7c803205d3572 Mon Sep 17 00:00:00 2001 From: Stevie Mayhew Date: Tue, 30 Jul 2019 09:42:10 +1200 Subject: [PATCH] Add an unpack command to allow ejection of code from recipes. --- README.md | 17 +++ composer.json | 3 +- src/{ => Command}/RecipeCommandBehaviour.php | 2 +- .../RequireRecipe.php} | 4 +- src/Command/UnpackCommand.php | 126 ++++++++++++++++++ .../UpdateRecipe.php} | 4 +- src/PackageResolver.php | 56 ++++++++ src/RecipeCommandProvider.php | 8 +- src/Unpack/Result.php | 41 ++++++ src/Unpack/Unpacker.php | 63 +++++++++ 10 files changed, 316 insertions(+), 8 deletions(-) rename src/{ => Command}/RecipeCommandBehaviour.php (99%) rename src/{RequireRecipeCommand.php => Command/RequireRecipe.php} (95%) create mode 100644 src/Command/UnpackCommand.php rename src/{UpdateRecipeCommand.php => Command/UpdateRecipe.php} (95%) create mode 100644 src/PackageResolver.php create mode 100644 src/Unpack/Result.php create mode 100644 src/Unpack/Unpacker.php diff --git a/README.md b/README.md index 8538a69..a45fb3b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ These recipes allow for the following features: - A `require-recipe` command to inline a recipe into the root composer.json, allowing the developer to customise the recipe dependencies without mandating the inclusion of all requirements directly. - An `update-recipe` command to upgrade to a newer version of a recipe. + - An `unpack` command to eject the recipe from the current project. ## Example output @@ -182,3 +183,19 @@ public/ blog.css composer.json ``` + +## Unpacking a recipe +Sometimes you need to remove the recipe configuration for more control over your dependency set. In these situations +you can unpack a SilverStripe recipe. + +To unpack a recipe you can run the following command +```shell +composer unpack silverstripe/recipe-core +``` + +This commands moves the composer requirements out of the recipe and puts them into the root `composer.json`. This allows +the ability to easily modify recipe requirements when you need to. + +###Credit +Parts of the unpack system were influenced by, or taken completely, from the work of Fabien Potencier on +[Symfony Flex](https://github.com/symfony/flex/blob/master/LICENSE). diff --git a/composer.json b/composer.json index e2f32a3..acd60c5 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "lint-clean": "phpcbf src/" }, "require": { - "composer-plugin-api": "^1.1" + "composer-plugin-api": "^1.1", + "ext-json": "*" }, "require-dev": { "composer/composer": "^1.2" diff --git a/src/RecipeCommandBehaviour.php b/src/Command/RecipeCommandBehaviour.php similarity index 99% rename from src/RecipeCommandBehaviour.php rename to src/Command/RecipeCommandBehaviour.php index 27cc83a..6d8e25e 100644 --- a/src/RecipeCommandBehaviour.php +++ b/src/Command/RecipeCommandBehaviour.php @@ -1,6 +1,6 @@ - See README.md for details + */ +class UnpackCommand extends BaseCommand +{ + protected function configure() + { + $this->setName('silverstripe:unpack') + ->setAliases(['unpack']) + ->setDescription('Unpacks a SilverStripe recipe.') + ->addArgument( + 'packages', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'Installed packages to unpack.' + ) + ->addUsage('silverstripe/recipe-core') + ->setHelp( + <<getComposer(); + $io = $this->getIO(); + + $resolver = new PackageResolver(); + $packages = $resolver->resolve($input->getArgument('packages'), true); + $json = new JsonFile(Factory::getComposerFile()); + $manipulator = new JsonConfigSource($json); + $locker = $composer->getLocker(); + $lockData = $locker->getLockData(); + $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); + $versionParser = new VersionParser(); + + $data = []; + foreach ($versionParser->parseNameVersionPairs($packages) as $package) { + if (null === $pkg = $installedRepo->findPackage($package['name'], '*')) { + $io->writeError(sprintf('Package %s is not installed', $package['name'])); + + return 1; + } + + $dev = false; + foreach ($lockData['packages-dev'] as $p) { + if ($package['name'] === $p['name']) { + $dev = true; + + break; + } + } + $data[] = [ + 'pkg' => $pkg, + 'dev' => $dev, + ]; + } + + $unpacker = new Unpacker($composer, $resolver); + $result = $unpacker->unpack($data); + + + // remove the packages themselves + if (!$result->getUnpacked()) { + $io->writeError('Nothing to unpack'); + + return; + } + + foreach ($result->getUnpacked() as $pkg) { + $io->writeError(sprintf('Unpacked %s dependencies', $pkg->getName())); + } + + foreach ($result->getUnpacked() as $package) { + $manipulator->removeLink('require-dev', $package->getName()); + foreach ($lockData['packages-dev'] as $i => $pkg) { + if ($package->getName() === $pkg['name']) { + unset($lockData['packages-dev'][$i]); + } + } + $manipulator->removeLink('require', $package->getName()); + foreach ($lockData['packages'] as $i => $pkg) { + if ($package->getName() === $pkg['name']) { + unset($lockData['packages'][$i]); + } + } + } + $lockData['packages'] = array_values($lockData['packages']); + $lockData['packages-dev'] = array_values($lockData['packages-dev']); + $lockData['content-hash'] = $locker->getContentHash(file_get_contents($json->getPath())); + $lockFile = new JsonFile(substr($json->getPath(), 0, -4) . 'lock', null, $io); + $lockFile->write($lockData); + + // force removal of files under vendor/ + $locker = new Locker($io, $lockFile, $composer->getRepositoryManager(), $composer->getInstallationManager(), file_get_contents($json->getPath())); + $composer->setLocker($locker); + $install = Installer::create($io, $composer); + $install + ->setDevMode(true) + ->setDumpAutoloader(false) + ->setRunScripts(false) + ->setSkipSuggest(true) + ->setIgnorePlatformRequirements(true); + + return $install->run(); + } +} diff --git a/src/UpdateRecipeCommand.php b/src/Command/UpdateRecipe.php similarity index 95% rename from src/UpdateRecipeCommand.php rename to src/Command/UpdateRecipe.php index 0d5fe57..06bfe24 100644 --- a/src/UpdateRecipeCommand.php +++ b/src/Command/UpdateRecipe.php @@ -1,6 +1,6 @@ - See README.md for details + */ +class PackageResolver +{ + private static $aliases = [ + 'core' => 'silverstripe/recipe-core' + ]; + + public function resolve(array $arguments = []): array + { + $versionParser = new VersionParser(); + + // first pass split on : and = to separate package names and versions + $explodedArguments = []; + foreach ($arguments as $argument) { + if ((false !== $pos = strpos($argument, ':')) || (false !== $pos = strpos($argument, '='))) { + $explodedArguments[] = substr($argument, 0, $pos); + $explodedArguments[] = substr($argument, $pos + 1); + } else { + $explodedArguments[] = $argument; + } + } + + // second pass to resolve package names + $packages = []; + foreach ($explodedArguments as $i => $argument) { + if (false === strpos($argument, '/') && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $argument) && !\in_array($argument, ['mirrors', 'nothing'])) { + + if (isset(self::$aliases[$argument])) { + $argument = self::$aliases[$argument]; + } else { + $versionParser->parseConstraints($argument); + } + } + + $packages[] = $argument; + } + + // third pass to resolve versions + $requires = []; + foreach ($versionParser->parseNameVersionPairs($packages) as $package) { + $requires[] = $package['name']; + } + + return array_unique($requires); + } + +} diff --git a/src/RecipeCommandProvider.php b/src/RecipeCommandProvider.php index 8e2c10e..4a442c4 100644 --- a/src/RecipeCommandProvider.php +++ b/src/RecipeCommandProvider.php @@ -4,6 +4,9 @@ namespace SilverStripe\RecipePlugin; use Composer\Command\BaseCommand; use Composer\Plugin\Capability\CommandProvider; +use SilverStripe\RecipePlugin\Command\RequireRecipe; +use SilverStripe\RecipePlugin\Command\UnpackCommand; +use SilverStripe\RecipePlugin\Command\UpdateRecipe; class RecipeCommandProvider implements CommandProvider { @@ -15,8 +18,9 @@ class RecipeCommandProvider implements CommandProvider public function getCommands() { return [ - new RequireRecipeCommand(), - new UpdateRecipeCommand(), + new RequireRecipe(), + new UpdateRecipe(), + new UnpackCommand(), ]; } } diff --git a/src/Unpack/Result.php b/src/Unpack/Result.php new file mode 100644 index 0000000..197b4bc --- /dev/null +++ b/src/Unpack/Result.php @@ -0,0 +1,41 @@ + - See README.md for details + */ +class Result +{ + private $unpacked = []; + private $required = []; + + public function addUnpacked(PackageInterface $package) + { + $this->unpacked[] = $package; + } + + /** + * @return PackageInterface[] + */ + public function getUnpacked(): array + { + return $this->unpacked; + } + + public function addRequired(string $package) + { + $this->required[] = $package; + } + + /** + * @return string[] + */ + public function getRequired(): array + { + // we need at least one package for the command to work properly + return $this->required ?: ['silverstripe/recipe-plugin']; + } +} diff --git a/src/Unpack/Unpacker.php b/src/Unpack/Unpacker.php new file mode 100644 index 0000000..119cbf9 --- /dev/null +++ b/src/Unpack/Unpacker.php @@ -0,0 +1,63 @@ + - See README.md for details + */ +class Unpacker +{ + private $composer; + private $resolver; + + public function __construct(Composer $composer, PackageResolver $resolver) + { + $this->composer = $composer; + $this->resolver = $resolver; + } + + public function unpack(array $packages): Result + { + $result = new Result(); + $json = new JsonFile(Factory::getComposerFile()); + $manipulator = new JsonManipulator(file_get_contents($json->getPath())); + foreach ($packages as $package) { + + $dev = $package['dev']; + $package = $package['pkg']; + + // not unpackable or no --unpack flag or empty packs (markers) + if ( + null === $package || + 'silverstripe-recipe' !== $package->getType() || + 0 === \count($package->getRequires()) + \count($package->getDevRequires()) + ) { + $result->addRequired($package->getName().($package->getVersion() ? ':'.$package->getVersion() : '')); + continue; + } + + $result->addUnpacked($package); + foreach ($package->getRequires() as $link) { + if ('php' === $link->getTarget()) { + continue; + } + + $constraint = $link->getPrettyConstraint(); + + if (!$manipulator->addLink($dev ? 'require-dev' : 'require', $link->getTarget(), $constraint)) { + throw new \RuntimeException(sprintf('Unable to unpack package "%s".', $link->getTarget())); + } + } + } + + file_put_contents($json->getPath(), $manipulator->getContents()); + + return $result; + } +}