8000 [DX] Command to list all available recipes · symfony/flex@f73fba0 · GitHub
[go: up one dir, main page]

Skip to content

Commit f73fba0

Browse files
maxheliasnicolas-grekas
authored andcommitted
[DX] Command to list all available recipes
1 parent 80c74cc commit f73fba0

File tree

5 files changed

+337
-8
lines changed

5 files changed

+337
-8
lines changed

src/Command/RecipesCommand.php

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 Symfony\Component\Console\Input\InputArgument;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Flex\InformationOperation;
19+
use Symfony\Flex\Lock;
20+
use Symfony\Flex\Recipe;
21+
22+
/**
23+
* @author Maxime Hélias <maximehelias16@gmail.com>
24+
*/
25+
class RecipesCommand extends BaseCommand
26+
{
27+
/** @var \Symfony\Flex\Flex */
28+
private $flex;
29+
30+
/** @var Lock */
31+
private $symfonyLock;
32+
33+
public function __construct(/* cannot be type-hinted */ $flex, Lock $symfonyLock)
34+
{
35+
$this->flex = $flex;
36+
$this->symfonyLock = $symfonyLock;
37+
38+
parent::__construct();
39+
}
40+
41+
protected function configure()
42+
{
43+
$this->setName('symfony:recipes')
44+
->setAliases(['recipes'])
45+
->setDescription('Shows information about all available recipes.')
46+
->setDefinition([
47+
new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect, if not provided all packages are.'),
48+
])
49+
;
50+
}
51+
52+
protected function execute(InputInterface $input, OutputInterface $output)
53+
{
54+
$installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
55+
56+
// Inspect one or all packages
57+
$package = $input->getArgument('package');
58+
if (null !== $package) {
59+
$packages = [0 => ['name' => strtolower($package)]];
60+
} else {
61+
$locker = $this->getComposer()->getLocker();
62+
$lockData = $locker->getLockData();
63+
64+
// Merge all packages installed
65+
$packages = array_merge($lockData['packages'], $lockData['packages-dev']);
66+
}
67+
68+
$operations = [];
69+
foreach ($packages as $value) {
70+
if (null === $pkg = $installedRepo->findPackage($value['name'], '*')) {
71+
$this->getIO()->writeError(sprintf('<error>Package %s is not installed</error>', $value['name']));
72+
73+
continue;
74+
}
75+
76+
$operations[] = new InformationOperation($pkg);
77+
}
78+
79+
$recipes = $this->flex->fetchRecipes($operations);
80+
ksort($recipes);
81+
82+
$nbRecipe = \count($recipes);
83+
if ($nbRecipe <= 0) {
84+
$this->getIO()->writeError('<error>No recipe found</error>');
85+
86+
return 1;
87+
}
88+
89+
// Display the information about a specific recipe
90+
if (1 === $nbRecipe) {
91+
$this->displayPackageInformation(current($recipes));
92+
93+
return 0;
94+
}
95+
96+
// display a resume of all packages
97+
$write = [
98+
'',
99+
'<bg=blue;fg=white> </>',
100+
'<bg=blue;fg=white> Available recipes. </>',
101+
'<bg=blue;fg=white> </>',
102+
'',
103+
];
104+
105+
/** @var Recipe $recipe */
106+
foreach ($recipes as $name => $recipe) {
107+
$lockRef = $this->symfonyLock->get($name)['recipe']['ref'] ?? null;
108+
109+
$additional = '';
110+
if (null === $lockRef && null !== $recipe->getRef()) {
111+
$additional = '<comment>(recipe not installed)</comment>';
112+
} elseif ($recipe->getRef() !== $lockRef) {
113+
$additional = '<comment>(update available)</comment>';
114+
}
115+
$write[] = sprintf(' * %s %s', $name, $additional);
116+
}
117+
118+
$write[] = '';
119+
$write[] = 'Run:';
120+
$write[] = ' * <info>composer recipes vendor/package</info> to see details about a recipe.';
121+
$write[] = ' * <info>composer recipes:install vendor/package --force -v</info> to update that recipe.';
122+
$write[] = '';
123+
124+
$this->getIO()->write($write);
125+
126+
return 0;
127+
}
128+
129+
private function displayPackageInformation(Recipe $recipe)
130+
{
131+
$recipeLock = $this->symfonyLock->get($recipe->getName());
132+
133+
$lockRef = $recipeLock['recipe']['ref'] ?? null;
134+
$lockFiles = $recipeLock['files'] ?? null;
135+
136+
$status = '<comment>up to date</comment>';
137+
if ($recipe->isAuto()) {
138+
$status = '<comment>auto-generated recipe</comment>';
139+
} elseif (null === $lockRef && null !== $recipe->getRef()) {
140+
$status = '<comment>recipe not installed</comment>';
141+
} elseif ($recipe->getRef() !== $lockRef) {
142+
$status = '<comment>update available</comment>';
143+
}
144+
145+
$io = $this->getIO();
146+
$io->write('<info>name</info> : '.$recipe->getName());
147+
$io->write('<info>version</info> : '.$recipeLock['version']);
148+
if (!$recipe->isAuto()) {
149+
$io->write('<info>repo</info> : '.sprintf('https://%s/tree/master/%s/%s', $recipeLock['recipe']['repo'], $recipe->getName(), $recipeLock['version']));
150+
}
151+
$io->write('<info>status</info> : '.$status);
152+
153+
if (null !== $lockFiles) {
154+
$io->write('<info>files</info> : ');
155+
$io->write('');
156+
157+
$tree = $this->generateFilesTree($lockFiles);
158+
159+
$this->displayFilesTree($tree);
160+
}
161+
162+
$io->write([
163+
'',
164+
'Update this recipe by running:',
165+
sprintf('<info>composer recipes:install %s --force -v</info>', $recipe->getName()),
166+
]);
167+
}
168+
169+
private function generateFilesTree(array $files): array
170+
{
171+
$tree = [];
172+
foreach ($files as $file) {
173+
$path = explode('/', $file);
174+
175+
$tree = array_merge_recursive($tree, $this->addNode($path));
176+
}
177+
178+
return $tree;
179+
}
180+
181+
private function addNode(array $node): array
182+
{
183+
$current = array_shift($node);
184+
185+
$subTree = [];
186+
if (null !== $current) {
187+
$subTree[$current] = $this->addNode($node);
188+
}
189+
190+
return $subTree;
191+
}
192+
193+
/**
194+
* Note : We do not display file modification information with Configurator like ComposerScripts, Container, DockerComposer, Dockerfile, Env, Gitignore and Makefile.
195+
*/
196+
private function displayFilesTree(array $tree)
197+
{
198+
$endKey = array_key_last($tree);
199+
foreach ($tree as $dir => $files) {
200+
$treeBar = '';
201+
$total = \count($files);
202+
if (0 === $total || $endKey === $dir) {
203+
$treeBar = '';
204+
}
205+
206+
$info = sprintf(
207+
'%s──%s',
208+
$treeBar,
209+
$dir
210+
);
211+
$this->writeTreeLine($info);
212+
213+
$treeBar = str_replace('', ' ', $treeBar);
214+
215+
$this->displayTree($files, $treeBar);
216+
}
217+
}
218+
219+
private function displayTree(array $tree, $previousTreeBar = '', $level = 1)
220+
{
221+
$previousTreeBar = str_replace('', '', $previousTreeBar);
222+
$treeBar = $previousTreeBar.'';
223+
224+
$i = 0;
225+
$total = \count($tree);
226+
227+
foreach ($tree as $dir => $files) {
228+
++$i;
229+
if ($i === $total) {
230+
$treeBar = $previousTreeBar.'';
231+
}
232+
233+
$info = sprintf(
234+
'%s──%s',
235+
$treeBar,
236+
$dir
237+
);
238+
$this->writeTreeLine($info);
239+
240+
$treeBar = str_replace('', ' ', $treeBar);
241+
242+
$this->displayTree($files, $treeBar, $level + 1);
243+
}
244+
}
245+
246+
private function writeTreeLine($line)
247+
{
248+
$io = $this->getIO();
249+
if (!$io->isDecorated()) {
250+
$line = str_replace(['', '', '──', ''], ['`-', '|-', '-', '|'], $line);
251+
}
252+
253+
$io->write($line);
254+
}
255+
}

src/Flex.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
243243
$app->add(new Command\UpdateCommand($resolver));
244244
$app->add(new Command\RemoveCommand($resolver));
245245
$app->add(new Command\UnpackCommand($resolver));
246+
$app->add(new Command\RecipesCommand($this, $this->lock));
246247
$app->add(new Command\InstallRecipesCommand($this, $this->options->get('root-dir')));
247248
$app->add(new Command\GenerateIdCommand($this));
248249
$app->add(new Command\DumpEnvCommand($this->config, $this->options));
@@ -402,7 +403,8 @@ public function install(Event $event = null)
402403
copy($rootDir.'/.env.dist', $rootDir.'/.env');
403404
}
404405

405-
$recipes = $this->fetchRecipes();
406+
$recipes = $this->fetchRecipes($this->operations);
407+
$this->operations = []; // Reset the operation after getting recipes
406408

407409
if (2 === $this->displayThanksReminder) {
408410
$love = '\\' === \DIRECTORY_SEPARATOR ? 'love' : '💖 ';
@@ -700,23 +702,23 @@ private function updateAutoloadFile()
700702
);
701703
}
702704

703-
private function fetchRecipes(): array
705+
public function fetchRecipes(array $operations): array
704706
{
705707
if (!$this->downloader->isEnabled()) {
706708
$this->io->writeError('<warning>Symfony recipes are disabled: "symfony/flex" not found in the root composer.json</>');
707709

708710
return [];
709711
}
710712
$devPackages = null;
711-
$data = $this->downloader->getRecipes($this->operations);
713+
$data = $this->downloader->getRecipes($operations);
712714
$manifests = $data['manifests'] ?? [];
713715
$locks = $data['locks'] ?? [];
714716
// symfony/flex and symfony/framework-bundle recipes should always be applied first
715717
$recipes = [
716718
'symfony/flex' => null,
717719
'symfony/framework-bundle' => null,
718720
];
719-
E377 foreach ($this->operations as $i => $operation) {
721+
foreach ($operations as $i => $operation) {
720722
if ($operation instanceof UpdateOperation) {
721723
$package = $operation->getTargetPackage();
722724
} else {
@@ -753,7 +755,7 @@ private function fetchRecipes(): array
753755
}
754756

755757
if (isset($manifests[$name])) {
756-
$recipes[$name] = new Recipe($package, $name, $job, $manifests[$name]);
758+
$recipes[$name] = new Recipe($package, $name, $job, $manifests[$name], $locks[$name] ?? []);
757759
}
758760

759761
$noRecipe = !isset($manifests[$name]) || (isset($manifests[$name]['not_installable']) && $manifests[$name]['not_installable']);
@@ -773,7 +775,7 @@ private function fetchRecipes(): array
773775
}
774776
}
775777
}
776-
$this->operations = [];
778+
$operations = [];
777779

778780
return array_filter($recipes);
779781
}

src/InformationOperation.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Symfony\Flex;
4+
5+
use Composer\DependencyResolver\Operation\SolverOperation;
6+
use Composer\Package\PackageInterface;
7+
8+
/**
9+
* @author Maxime Hélias <maximehelias16@gmail.com>
10+
*/
11+
class InformationOperation extends SolverOperation
12+
{
13+
private $package;
14+
15+
public function __construct(PackageInterface $package, $reason = null)
16+
{
17+
parent::__construct($reason);
18+
19+
$this->package = $package;
20+
}
21+
22+
/**
23+
* Returns package instance.
24+
*
25+
* @return PackageInterface
26+
*/
27+
public function getPackage()
28+
{
29+
return $this->package;
30+
}
31+
32+
/**
33+
* Returns job type.
34+
*
35+
* @return string
36+
*/
37+
public function getJobType()
38+
{
39+
return 'information';
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function __toString()
46+
{
47+
return 'Information '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')';
48+
}
49+
}

0 commit comments

Comments
 (0)
0