Skip to content

Add support for unpacking Composer packages #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/Command/RequireCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
namespace Symfony\Flex\Command;

use Composer\Command\RequireCommand as BaseRequireCommand;
use Composer\Package\Version\VersionParser;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\PackageResolver;
use Symfony\Flex\Unpacker;
use Symfony\Flex\Unpack\Operation;

class RequireCommand extends BaseRequireCommand
{
Expand All @@ -27,9 +31,30 @@ public function __construct(PackageResolver $resolver)
parent::__construct();
}

protected function configure()
{
parent::configure();
$this->addOption('unpack', null, InputOption::VALUE_NONE, 'Unpack Symfony packs in composer.json.');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$input->setArgument('packages', $this->resolver->resolve($input->getArgument('packages'), true));
$packages = $this->resolver->resolve($input->getArgument('packages'), true);

$versionParser = new VersionParser();
$op = new Operation($input->getOption('unpack'), $input->getOption('sort-packages') || $this->getComposer()->getConfig()->get('sort-packages'));
foreach ($versionParser->parseNameVersionPairs($packages) as $package) {
$op->addPackage($package['name'], $package['version'] ?? '', $input->getOption('dev'));
}

$unpacker = new Unpacker($this->getComposer());
$result = $unpacker->unpack($op);
$io = $this->getIo();
foreach ($result->getUnpacked() as $pkg) {
$io->writeError(sprintf('<info>Unpacked %s dependencies</>', $pkg->getName()));
}

$input->setArgument('packages', $result->getRequired());

if ($input->hasOption('no-suggest')) {
$input->setOption('no-suggest', true);
Expand Down
128 changes: 128 additions & 0 deletions src/Command/UnpackCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Flex\Command;

use Composer\Command\BaseCommand;
use Composer\Config\JsonConfigSource;
use Composer\Factory;
use Composer\Installer;
use Composer\Json\JsonFile;
use Composer\Package\Locker;
use Composer\Package\Version\VersionParser;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Flex\PackageResolver;
use Symfony\Flex\Unpacker;
use Symfony\Flex\Unpack\Operation;

class UnpackCommand extends BaseCommand
{
public function __construct(PackageResolver $resolver)
{
$this->resolver = $resolver;

parent::__construct();
}

protected function configure()
{
$this->setName('unpack')
->setDescription('Unpack a Symfony pack.')
->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Installed packages to unpack.'),
new InputOption('sort-packages', null, InputOption::VALUE_NONE, 'Sorts packages'),
))
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$composer = $this->getComposer();
$packages = $this->resolver->resolve($input->getArgument('packages'), true);
$io = $this->getIo();
$json = new JsonFile(Factory::getComposerFile());
$manipulator = new JsonConfigSource($json);
$locker = $composer->getLocker();
$lockData = $locker->getLockData();
$installedRepo = $composer->getRepositoryManager()->getLocalRepository();
$versionParser = new VersionParser();

$op = new Operation(true, $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages'));
foreach ($versionParser->parseNameVersionPairs($packages) as $package) {
if (null === $pkg = $installedRepo->findPackage($package['name'], '*')) {
$io->writeError(sprintf('<error>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;
}
}

$op->addPackage($package['name'], '*', $dev);
}

$unpacker = new Unpacker($composer);
$result = $unpacker->unpack($op);

// remove the packages themselves
if (!$result->getUnpacked()) {
$io->writeError('<info>Nothing to unpack</>');
return;
}

foreach ($result->getUnpacked() as $pkg) {
$io->writeError(sprintf('<info>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();
}
}
1 change: 1 addition & 0 deletions src/Flex.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public function activate(Composer $composer, IOInterface $io)
$app->add(new Command\RequireCommand($resolver));
$app->add(new Command\UpdateCommand($resolver));
$app->add(new Command\RemoveCommand($resolver));
$app->add(new Command\UnpackCommand($resolver));
} elseif ($trace['object'] instanceof Installer) {
--$search;
$trace['object']->setSuggestedPackagesReporter(new SuggestedPackagesReporter(new NullIO()));
Expand Down
49 changes: 49 additions & 0 deletions src/Unpack/Operation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Flex\Unpack;

class Operation
{
private $packages;
private $unpack;
private $sort;

public function __construct(bool $unpack, bool $sort)
{
$this->unpack = $unpack;
$this->sort = $sort;
}

public function addPackage(string $name, string $version, bool $dev)
{
$this->packages[] = [
'name' => $name,
'version' => $version,
'dev' => $dev,
];
}

public function getPackages(): array
{
return $this->packages;
}

public function shouldUnpack(): bool
{
return $this->unpack;
}

public function shouldSort(): bool
{
return $this->sort;
}
}
47 changes: 47 additions & 0 deletions src/Unpack/Result.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Flex\Unpack;

use Composer\Package\PackageInterface;

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 ?: ['symfony/flex'];
}
}
68 changes: 68 additions & 0 deletions src/Unpacker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Flex;

use Composer\Composer;
use Composer\Factory;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Package\Link;
use Composer\Package\Package;
use Symfony\Flex\PackageResolver;
use Symfony\Flex\Unpack\Operation;
use Symfony\Flex\Unpack\Result;

class Unpacker
{
private $composer;

public function __construct(Composer $composer)
{
$this->composer = $composer;
}

public function unpack(Operation $op): Result
{
$result = new Result();
$json = new JsonFile(Factory::getComposerFile());
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
foreach ($op->getPackages() as $package) {
$pkg = $this->composer->getRepositoryManager()->findPackage($package['name'], $package['version'] ?: '*');

// not unpackable or no --unpack flag or empty packs (markers)
if (
'symfony-pack' !== $pkg->getType() ||
!$op->shouldUnpack() ||
0 === count($pkg->getRequires()) + count($pkg->getDevRequires())
) {
$result->addRequired($package['name'].($package['version'] ? ':'.$package['version'] : ''));

continue;
}

$result->addUnpacked($pkg);
foreach ($pkg->getRequires() as $link) {
if ('php' === $link->getTarget()) {
continue;
}

if (!$manipulator->addLink($package['dev'] ? 'require-dev' : 'require', $link->getTarget(), $link->getPrettyConstraint(), $op->shouldSort())) {
throw new \RuntimeException(sprintf('Unable to unpack package "%s".', $link->getTarget()));
}
}
}

file_put_contents($json->getPath(), $manipulator->getContents());

return $result;
}
}