Skip to content

Commit

Permalink
feature symfony#241 Add support for unpacking Composer packages (fabpot)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 1.0-dev branch (closes symfony#241).

Discussion
----------

Add support for unpacking Composer packages

This PR is short but packed (pun intended) with great features.

Basically, it helps fix an issue people have with Flex: starting a new project.

Commits
-------

e4bdd74 Add support for unpacking Composer packages
  • Loading branch information
fabpot committed Dec 20, 2017
2 parents 706ea52 + e4bdd74 commit adb823e
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 1 deletion.
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;
}
}

0 comments on commit adb823e

Please sign in to comment.