Skip to content

Commit adb823e

Browse files
committed
feature #241 Add support for unpacking Composer packages (fabpot)
This PR was squashed before being merged into the 1.0-dev branch (closes #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
2 parents 706ea52 + e4bdd74 commit adb823e

File tree

6 files changed

+319
-1
lines changed

6 files changed

+319
-1
lines changed

src/Command/RequireCommand.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212
namespace Symfony\Flex\Command;
1313

1414
use Composer\Command\RequireCommand as BaseRequireCommand;
15+
use Composer\Package\Version\VersionParser;
1516
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
1618
use Symfony\Component\Console\Output\OutputInterface;
1719
use Symfony\Flex\PackageResolver;
20+
use Symfony\Flex\Unpacker;
21+
use Symfony\Flex\Unpack\Operation;
1822

1923
class RequireCommand extends BaseRequireCommand
2024
{
@@ -27,9 +31,30 @@ public function __construct(PackageResolver $resolver)
2731
parent::__construct();
2832
}
2933

34+
protected function configure()
35+
{
36+
parent::configure();
37+
$this->addOption('unpack', null, InputOption::VALUE_NONE, 'Unpack Symfony packs in composer.json.');
38+
}
39+
3040
protected function execute(InputInterface $input, OutputInterface $output)
3141
{
32-
$input->setArgument('packages', $this->resolver->resolve($input->getArgument('packages'), true));
42+
$packages = $this->resolver->resolve($input->getArgument('packages'), true);
43+
44+
$versionParser = new VersionParser();
45+
$op = new Operation($input->getOption('unpack'), $input->getOption('sort-packages') || $this->getComposer()->getConfig()->get('sort-packages'));
46+
foreach ($versionParser->parseNameVersionPairs($packages) as $package) {
47+
$op->addPackage($package['name'], $package['version'] ?? '', $input->getOption('dev'));
48+
}
49+
50+
$unpacker = new Unpacker($this->getComposer());
51+
$result = $unpacker->unpack($op);
52+
$io = $this->getIo();
53+
foreach ($result->getUnpacked() as $pkg) {
54+
$io->writeError(sprintf('<info>Unpacked %s dependencies</>', $pkg->getName()));
55+
}
56+
57+
$input->setArgument('packages', $result->getRequired());
3358

3459
if ($input->hasOption('no-suggest')) {
3560
$input->setOption('no-suggest', true);

src/Command/UnpackCommand.php

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Flex\Command;
13+
14+
use Composer\Command\BaseCommand;
15+
use Composer\Config\JsonConfigSource;
16+
use Composer\Factory;
17+
use Composer\Installer;
18+
use Composer\Json\JsonFile;
19+
use Composer\Package\Locker;
20+
use Composer\Package\Version\VersionParser;
21+
use Symfony\Component\Console\Input\InputArgument;
22+
use Symfony\Component\Console\Input\InputInterface;
23+
use Symfony\Component\Console\Input\InputOption;
24+
use Symfony\Component\Console\Output\OutputInterface;
25+
use Symfony\Flex\PackageResolver;
26+
use Symfony\Flex\Unpacker;
27+
use Symfony\Flex\Unpack\Operation;
28+
29+
class UnpackCommand extends BaseCommand
30+
{
31+
public function __construct(PackageResolver $resolver)
32+
{
33+
$this->resolver = $resolver;
34+
35+
parent::__construct();
36+
}
37+
38+
protected function configure()
39+
{
40+
$this->setName('unpack')
41+
->setDescription('Unpack a Symfony pack.')
42+
->setDefinition(array(
43+
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Installed packages to unpack.'),
44+
new InputOption('sort-packages', null, InputOption::VALUE_NONE, 'Sorts packages'),
45+
))
46+
;
47+
}
48+
49+
protected function execute(InputInterface $input, OutputInterface $output)
50+
{
51+
$composer = $this->getComposer();
52+
$packages = $this->resolver->resolve($input->getArgument('packages'), true);
53+
$io = $this->getIo();
54+
$json = new JsonFile(Factory::getComposerFile());
55+
$manipulator = new JsonConfigSource($json);
56+
$locker = $composer->getLocker();
57+
$lockData = $locker->getLockData();
58+
$installedRepo = $composer->getRepositoryManager()->getLocalRepository();
59+
$versionParser = new VersionParser();
60+
61+
$op = new Operation(true, $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages'));
62+
foreach ($versionParser->parseNameVersionPairs($packages) as $package) {
63+
if (null === $pkg = $installedRepo->findPackage($package['name'], '*')) {
64+
$io->writeError(sprintf('<error>Package %s is not installed</>', $package['name']));
65+
66+
return 1;
67+
}
68+
69+
$dev = false;
70+
foreach ($lockData['packages-dev'] as $p) {
71+
if ($package['name'] === $p['name']) {
72+
$dev = true;
73+
74+
break;
75+
}
76+
}
77+
78+
$op->addPackage($package['name'], '*', $dev);
79+
}
80+
81+
$unpacker = new Unpacker($composer);
82+
$result = $unpacker->unpack($op);
83+
84+
// remove the packages themselves
85+
if (!$result->getUnpacked()) {
86+
$io->writeError('<info>Nothing to unpack</>');
87+
return;
88+
}
89+
90+
foreach ($result->getUnpacked() as $pkg) {
91+
$io->writeError(sprintf('<info>Unpacked %s dependencies</>', $pkg->getName()));
92+
}
93+
94+
foreach ($result->getUnpacked() as $package) {
95+
$manipulator->removeLink('require-dev', $package->getName());
96+
foreach ($lockData['packages-dev'] as $i => $pkg) {
97+
if ($package->getName() === $pkg['name']) {
98+
unset($lockData['packages-dev'][$i]);
99+
}
100+
}
101+
$manipulator->removeLink('require', $package->getName());
102+
foreach ($lockData['packages'] as $i => $pkg) {
103+
if ($package->getName() === $pkg['name']) {
104+
unset($lockData['packages'][$i]);
105+
}
106+
}
107+
}
108+
$lockData['packages'] = array_values($lockData['packages']);
109+
$lockData['packages-dev'] = array_values($lockData['packages-dev']);
110+
$lockData['content-hash'] = $locker->getContentHash(file_get_contents($json->getPath()));
111+
$lockFile = new JsonFile(substr($json->getPath(), 0, -4).'lock', null, $io);
112+
$lockFile->write($lockData);
113+
114+
// force removal of files under vendor/
115+
$locker = new Locker($io, $lockFile, $composer->getRepositoryManager(), $composer->getInstallationManager(), file_get_contents($json->getPath()));
116+
$composer->setLocker($locker);
117+
$install = Installer::create($io, $composer);
118+
$install
119+
->setDevMode(true)
120+
->setDumpAutoloader(false)
121+
->setRunScripts(false)
122+
->setSkipSuggest(true)
123+
->setIgnorePlatformRequirements(true)
124+
;
125+
126+
return $install->run();
127+
}
128+
}

src/Flex.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public function activate(Composer $composer, IOInterface $io)
8686
$app->add(new Command\RequireCommand($resolver));
8787
$app->add(new Command\UpdateCommand($resolver));
8888
$app->add(new Command\RemoveCommand($resolver));
89+
$app->add(new Command\UnpackCommand($resolver));
8990
} elseif ($trace['object'] instanceof Installer) {
9091
--$search;
9192
$trace['object']->setSuggestedPackagesReporter(new SuggestedPackagesReporter(new NullIO()));

src/Unpack/Operation.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Flex\Unpack;
13+
14+
class Operation
15+
{
16+
private $packages;
17+
private $unpack;
18+
private $sort;
19+
20+
public function __construct(bool $unpack, bool $sort)
21+
{
22+
$this->unpack = $unpack;
23+
$this->sort = $sort;
24+
}
25+
26+
public function addPackage(string $name, string $version, bool $dev)
27+
{
28+
$this->packages[] = [
29+
'name' => $name,
30+
'version' => $version,
31+
'dev' => $dev,
32+
];
33+
}
34+
35+
public function getPackages(): array
36+
{
37+
return $this->packages;
38+
}
39+
40+
public function shouldUnpack(): bool
41+
{
42+
return $this->unpack;
43+
}
44+
45+
public function shouldSort(): bool
46+
{
47+
return $this->sort;
48+
}
49+
}

src/Unpack/Result.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Flex\Unpack;
13+
14+
use Composer\Package\PackageInterface;
15+
16+
class Result
17+
{
18+
private $unpacked = [];
19+
private $required = [];
20+
21+
public function addUnpacked(PackageInterface $package)
22+
{
23+
$this->unpacked[] = $package;
24+
}
25+
26+
/**
27+
* @return PackageInterface[]
28+
*/
29+
public function getUnpacked(): array
30+
{
31+
return $this->unpacked;
32+
}
33+
34+
public function addRequired(string $package)
35+
{
36+
$this->required[] = $package;
37+
}
38+
39+
/**
40+
* @return string[]
41+
*/
42+
public function getRequired(): array
43+
{
44+
// we need at least one package for the command to work properly
45+
return $this->required ?: ['symfony/flex'];
46+
}
47+
}

src/Unpacker.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Flex;
13+
14+
use Composer\Composer;
15+
use Composer\Factory;
16+
use Composer\Json\JsonFile;
17+
use Composer\Json\JsonManipulator;
18+
use Composer\Package\Link;
19+
use Composer\Package\Package;
20+
use Symfony\Flex\PackageResolver;
21+
use Symfony\Flex\Unpack\Operation;
22+
use Symfony\Flex\Unpack\Result;
23+
24+
class Unpacker
25+
{
26+
private $composer;
27+
28+
public function __construct(Composer $composer)
29+
{
30+
$this->composer = $composer;
31+
}
32+
33+
public function unpack(Operation $op): Result
34+
{
35+
$result = new Result();
36+
$json = new JsonFile(Factory::getComposerFile());
37+
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
38+
foreach ($op->getPackages() as $package) {
39+
$pkg = $this->composer->getRepositoryManager()->findPackage($package['name'], $package['version'] ?: '*');
40+
41+
// not unpackable or no --unpack flag or empty packs (markers)
42+
if (
43+
'symfony-pack' !== $pkg->getType() ||
44+
!$op->shouldUnpack() ||
45+
0 === count($pkg->getRequires()) + count($pkg->getDevRequires())
46+
) {
47+
$result->addRequired($package['name'].($package['version'] ? ':'.$package['version'] : ''));
48+
49+
continue;
50+
}
51+
52+
$result->addUnpacked($pkg);
53+
foreach ($pkg->getRequires() as $link) {
54+
if ('php' === $link->getTarget()) {
55+
continue;
56+
}
57+
58+
if (!$manipulator->addLink($package['dev'] ? 'require-dev' : 'require', $link->getTarget(), $link->getPrettyConstraint(), $op->shouldSort())) {
59+
throw new \RuntimeException(sprintf('Unable to unpack package "%s".', $link->getTarget()));
60+
}
61+
}
62+
}
63+
64+
file_put_contents($json->getPath(), $manipulator->getContents());
65+
66+
return $result;
67+
}
68+
}

0 commit comments

Comments
 (0)