Skip to content

Commit 72c4bc7

Browse files
Merge branch '1.x' into 2.x
* 1.x: Fix checking for "flex-require-dev" in composer.json Fix reading recipe version from lock file Hit raw.githubusercontent.com to work around rate limiting Shorten Symfony Recipe URL Cache Keys Interactively asking if the user wants Docker support from Flex
2 parents f9a7a5e + 99e3323 commit 72c4bc7

File tree

9 files changed

+196
-39
lines changed

9 files changed

+196
-39
lines changed

src/Command/RecipesCommand.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ private function displayPackageInformation(Recipe $recipe)
156156
$lockRepo = $recipeLock['recipe']['repo'] ?? null;
157157
$lockFiles = $recipeLock['files'] ?? null;
158158
$lockBranch = $recipeLock['recipe']['branch'] ?? null;
159+
$lockVersion = $recipeLock['recipe']['version'] ?? $recipeLock['version'] ?? null;
159160

160161
$status = '<comment>up to date</comment>';
161162
if ($recipe->isAuto()) {
@@ -174,7 +175,7 @@ private function displayPackageInformation(Recipe $recipe)
174175
$recipe->getName(),
175176
$lockRepo,
176177
$lockBranch ?? '',
177-
$recipeLock['version'],
178+
$lockVersion,
178179
$lockRef
179180
);
180181
} catch (TransportException $exception) {
@@ -183,16 +184,16 @@ private function displayPackageInformation(Recipe $recipe)
183184
}
184185

185186
$io->write('<info>name</info> : '.$recipe->getName());
186-
$io->write('<info>version</info> : '.($recipeLock['version'] ?? 'n/a'));
187+
$io->write('<info>version</info> : '.($lockVersion ?? 'n/a'));
187188
$io->write('<info>status</info> : '.$status);
188-
if (!$recipe->isAuto() && isset($recipeLock['version'])) {
189+
if (!$recipe->isAuto() && null !== $lockVersion) {
189190
$recipeUrl = sprintf(
190191
'https://%s/tree/%s/%s/%s',
191192
$lockRepo,
192193
// if something fails, default to the branch as the closest "sha"
193194
$gitSha ?? $lockBranch,
194195
$recipe->getName(),
195-
$recipeLock['version']
196+
$lockVersion
196197
);
197198

198199
$io->write('<info>installed recipe</info> : '.$recipeUrl);
@@ -202,7 +203,7 @@ private function displayPackageInformation(Recipe $recipe)
202203
$io->write('<info>latest recipe</info> : '.$recipe->getURL());
203204
}
204205

205-
if ($lockRef !== $recipe->getRef() && isset($recipeLock['version'])) {
206+
if ($lockRef !== $recipe->getRef() && null !== $lockVersion) {
206207
$historyUrl = sprintf(
207208
'https://%s/commits/%s/%s',
208209
$lockRepo,

src/Configurator/DockerComposeConfigurator.php

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
namespace Symfony\Flex\Configurator;
1313

1414
use Composer\Composer;
15+
use Composer\Factory;
1516
use Composer\IO\IOInterface;
17+
use Composer\Json\JsonFile;
18+
use Composer\Json\JsonManipulator;
1619
use Symfony\Component\Filesystem\Filesystem;
1720
use Symfony\Flex\Lock;
1821
use Symfony\Flex\Options;
@@ -27,6 +30,8 @@ class DockerComposeConfigurator extends AbstractConfigurator
2730
{
2831
private $filesystem;
2932

33+
public static $configureDockerRecipes = null;
34+
3035
public function __construct(Composer $composer, IOInterface $io, Options $options)
3136
{
3237
parent::__construct($composer, $io, $options);
@@ -36,8 +41,7 @@ public function __construct(Composer $composer, IOInterface $io, Options $option
3641

3742
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
3843
{
39-
$installDocker = $this->composer->getPackage()->getExtra()['symfony']['docker'] ?? true;
40-
if (!$installDocker) {
44+
if (!self::shouldConfigureDockerRecipe($this->composer, $this->io, $recipe)) {
4145
return;
4246
}
4347

@@ -128,6 +132,72 @@ public function unconfigure(Recipe $recipe, $config, Lock $lock)
128132
$this->write('Docker Compose definitions have been modified. Please run "docker-compose up" again to apply the changes.');
129133
}
130134

135+
public static function shouldConfigureDockerRecipe(Composer $composer, IOInterface $io, Recipe $recipe): bool
136+
{
137+
if (null !== self::$configureDockerRecipes) {
138+
return self::$configureDockerRecipes;
139+
}
140+
141+
if (null !== $dockerPreference = $composer->getPackage()->getExtra()['symfony']['docker'] ?? null) {
142+
self::$configureDockerRecipes = $dockerPreference;
143+
144+
return self::$configureDockerRecipes;
145+
}
146+
147+
if ('install' !== $recipe->getJob()) {
148+
// default to not configuring
149+
return false;
150+
}
151+
152+
$warning = $io->isInteractive() ? 'WARNING' : 'IGNORING';
153+
$io->writeError(sprintf(' - <warning> %s </> %s', $warning, $recipe->getFormattedOrigin()));
154+
$question = ' The recipe for this package contains some Docker configuration.
155+
156+
This may create/update <comment>docker-compose.yml</comment> or update <comment>Dockerfile</comment> (if it exists).
157+
158+
Do you want to include Docker configuration from recipes?
159+
[<comment>y</>] Yes
160+
[<comment>n</>] No
161+
[<comment>p</>] Yes permanently, never ask again for this project
162+
[<comment>x</>] No permanently, never ask again for this project
163+
(defaults to <comment>y</>): ';
164+
$answer = $io->askAndValidate(
165+
$question,
166+
function ($value) {
167+
if (null === $value) {
168+
return 'y';
169+
}
170+
$value = strtolower($value[0]);
171+
if (!\in_array($value, ['y', 'n', 'p', 'x'], true)) {
172+
throw new \InvalidArgumentException('Invalid choice.');
173+
}
174+
175+
return $value;
176+
},
177+
null,
178+
'y'
179+
);
180+
if ('n' === $answer) {
181+
self::$configureDockerRecipes = false;
182+
183+
return self::$configureDockerRecipes;
184+
}
185+
if ('y' === $answer) {
186+
self::$configureDockerRecipes = true;
187+
188+
return self::$configureDockerRecipes;
189+
}
190+
191+
// yes or no permanently
192+
self::$configureDockerRecipes = 'p' === $answer;
193+
$json = new JsonFile(Factory::getComposerFile());
194+
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
195+
$manipulator->addSubNode('extra', 'symfony.docker', self::$configureDockerRecipes);
196+
file_put_contents($json->getPath(), $manipulator->getContents());
197+
198+
return self::$configureDockerRecipes;
199+
}
200+
131201
/**
132202
* Normalizes the config and return the name of the main Docker Compose file if applicable.
133203
*/

src/Configurator/DockerfileConfigurator.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ class DockerfileConfigurator extends AbstractConfigurator
2323
{
2424
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
2525
{
26-
$installDocker = $this->composer->getPackage()->getExtra()['symfony']['docker'] ?? true;
27-
if (!$installDocker) {
26+
if (!DockerComposeConfigurator::shouldConfigureDockerRecipe($this->composer, $this->io, $recipe)) {
2827
return;
2928
}
3029

src/Downloader.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
class Downloader
3030
{
3131
private const DEFAULT_ENDPOINTS = [
32-
'https://api.github.com/repos/symfony/recipes/contents/index.json?ref=flex/main',
33-
'https://api.github.com/repos/symfony/recipes-contrib/contents/index.json?ref=flex/main',
32+
'https://raw.githubusercontent.com/symfony/recipes/flex/main/index.json',
33+
'https://raw.githubusercontent.com/symfony/recipes-contrib/flex/main/index.json',
3434
];
3535
private const MAX_LENGTH = 1000;
3636

@@ -271,7 +271,7 @@ private function get(array $urls, bool $isRecipe = false, int $try = 3): array
271271
$options = [];
272272

273273
foreach ($urls as $url) {
274-
$cacheKey = preg_replace('{[^a-z0-9.]}i', '-', $url);
274+
$cacheKey = self::generateCacheKey($url);
275275
$headers = [];
276276

277277
if (preg_match('{^https?://api\.github\.com/}', $url)) {
@@ -300,7 +300,7 @@ private function get(array $urls, bool $isRecipe = false, int $try = 3): array
300300
foreach ($urls as $url) {
301301
$jobs[] = $this->rfs->add($url, $options[$url])->then(function (ComposerResponse $response) use ($url, &$responses) {
302302
if (200 === $response->getStatusCode()) {
303-
$cacheKey = preg_replace('{[^a-z0-9.]}i', '-', $url);
303+
$cacheKey = self::generateCacheKey($url);
304304
$responses[$url] = $this->parseJson($response->getBody(), $url, $cacheKey, $response->getHeaders())->getBody();
305305
}
306306
}, function (\Exception $e) use ($url, &$retries) {
@@ -314,7 +314,7 @@ private function get(array $urls, bool $isRecipe = false, int $try = 3): array
314314
}
315315
$this->rfs->download($urls, function ($url) use ($options, &$responses, &$retries, &$error) {
316316
try {
317-
$cacheKey = preg_replace('{[^a-z0-9.]}i', '-', $url);
317+
$cacheKey = self::generateCacheKey($url);
318318
$origin = method_exists($this->rfs, 'getOrigin') ? $this->rfs::getOrigin($url) : parse_url($url, \PHP_URL_HOST);
319319
$json = $this->rfs->getContents($origin, $url, false, $options[$url]);
320320
if (200 === $this->rfs->findStatusCode($this->rfs->getLastHeaders())) {
@@ -412,4 +412,12 @@ private function initialize()
412412
$this->endpoints[$endpoint] = $config;
413413
}
414414
}
415+
416+
private static function generateCacheKey(string $url): string
417+
{
418+
$url = preg_replace('{^https://api.github.com/repos/([^/]++/[^/]++)/contents/}', '$1/', $url);
419+
$url = preg_replace('{^https://raw.githubusercontent.com/([^/]++/[^/]++)/}', '$1/', $url);
420+
421+
return preg_replace('{[^a-z0-9.]}i', '-', $url);
422+
}
415423
}

src/Flex.php

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ class Flex implements PluginInterface, EventSubscriberInterface
9797
'remove' => false,
9898
'unpack' => true,
9999
];
100-
private $shouldUpdateComposerLock = false;
101100
private $filter;
102101

103102
public function activate(Composer $composer, IOInterface $io)
@@ -357,7 +356,7 @@ public function update(Event $event, $operations = [])
357356
$contents = file_get_contents($file);
358357
$json = JsonFile::parseJson($contents);
359358

360-
if (!isset($json['flex-require']) && !isset($json['flex-require'])) {
359+
if (!isset($json['flex-require']) && !isset($json['flex-require-dev'])) {
361360
$this->unpack($event);
362361

363362
return;
@@ -427,10 +426,11 @@ public function install(Event $event)
427426
$this->io->writeError(sprintf('<info>Symfony operations: %d recipe%s (%s)</>', \count($recipes), \count($recipes) > 1 ? 's' : '', $this->downloader->getSessionId()));
428427
$installContribs = $this->composer->getPackage()->getExtra()['symfony']['allow-contrib'] ?? false;
429428
$manifest = null;
429+
$originalComposerJsonHash = $this->getComposerJsonHash();
430430
foreach ($recipes as $recipe) {
431431
if ('install' === $recipe->getJob() && !$installContribs && $recipe->isContrib()) {
432432
$warning = $this->io->isInteractive() ? 'WARNING' : 'IGNORING';
433-
$this->io->writeError(sprintf(' - <warning> %s </> %s', $warning, $this->formatOrigin($recipe->getOrigin())));
433+
$this->io->writeError(sprintf(' - <warning> %s </> %s', $warning, $recipe->getFormattedOrigin()));
434434
$question = sprintf(' The recipe for this package comes from the "contrib" repository, which is open to community contributions.
435435
Review the recipe at %s
436436
@@ -468,13 +468,12 @@ function ($value) {
468468
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
469469
$manipulator->addSubNode('extra', 'symfony.allow-contrib', true);
470470
file_put_contents($json->getPath(), $manipulator->getContents());
471-
$this->shouldUpdateComposerLock = true;
472471
}
473472
}
474473

475474
switch ($recipe->getJob()) {
476475
case 'install':
477-
$this->io->writeError(sprintf(' - Configuring %s', $this->formatOrigin($recipe->getOrigin())));
476+
$this->io->writeError(sprintf(' - Configuring %s', $recipe->getFormattedOrigin()));
478477
$this->configurator->install($recipe, $this->lock, [
479478
'force' => $event instanceof UpdateEvent && $event->force(),
480479
]);
@@ -491,7 +490,7 @@ function ($value) {
491490
case 'update':
492491
break;
493492
case 'uninstall':
494-
$this->io->writeError(sprintf(' - Unconfiguring %s', $this->formatOrigin($recipe->getOrigin())));
493+
$this->io->writeError(sprintf(' - Unconfiguring %s', $recipe->getFormattedOrigin()));
495494
$this->configurator->unconfigure($recipe, $this->lock);
496495
break;
497496
}
@@ -512,7 +511,7 @@ function ($value) {
512511
$this->synchronizePackageJson($rootDir);
513512
$this->lock->write();
514513

515-
if ($this->shouldUpdateComposerLock) {
514+
if ($this->getComposerJsonHash() !== $originalComposerJsonHash) {
516515
$this->updateComposerLock();
517516
}
518517
}
@@ -808,16 +807,6 @@ private function initOptions(): Options
808807
return new Options($options, $this->io);
809808
}
810809

811-
private function formatOrigin(string $origin): string
812-
{
813-
// symfony/translation:[email protected]/symfony/recipes:branch
814-
if (!preg_match('/^([^:]++):([^@]++)@(.+)$/', $origin, $matches)) {
815-
return $origin;
816-
}
817-
818-
return sprintf('<info>%s</> (<comment>>=%s</>): From %s', $matches[1], $matches[2], 'auto-generated recipe' === $matches[3] ? '<comment>'.$matches[3].'</>' : $matches[3]);
819-
}
820-
821810
private function shouldRecordOperation(PackageEvent $event): bool
822811
{
823812
$operation = $event->getOperation();
@@ -981,4 +970,9 @@ public static function getSubscribedEvents(): array
981970

982971
return $events;
983972
}
973+
974+
private function getComposerJsonHash(): string
975+
{
976+
return md5_file(Factory::getComposerFile());
977+
}
984978
}

src/Options.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function shouldWriteFile(string $file, bool $overwrite): bool
6868
exec('git status --short --ignored --untracked-files=all -- '.ProcessExecutor::escape($file).' 2>&1', $output, $status);
6969

7070
if (0 !== $status) {
71-
return (bool) $this->io && $this->io->askConfirmation(sprintf('Cannot determine the state of the "%s" file, overwrite anyway? [y/N] ', $file), false);
71+
return $this->io && $this->io->askConfirmation(sprintf('Cannot determine the state of the "%s" file, overwrite anyway? [y/N] ', $file), false);
7272
}
7373

7474
if (empty($output[0]) || preg_match('/^[ AMDRCU][ D][ \t]/', $output[0])) {
@@ -78,7 +78,7 @@ public function shouldWriteFile(string $file, bool $overwrite): bool
7878
$name = basename($file);
7979
$name = \strlen($output[0]) - \strlen($name) === strrpos($output[0], $name) ? substr($output[0], 3) : $name;
8080

81-
return (bool) $this->io && $this->io->askConfirmation(sprintf('File "%s" has uncommitted changes, overwrite? [y/N] ', $name), false);
81+
return $this->io && $this->io->askConfirmation(sprintf('File "%s" has uncommitted changes, overwrite? [y/N] ', $name), false);
8282
}
8383

8484
public function toArray(): array

src/Recipe.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ public function getOrigin(): string
6767
return $this->data['origin'] ?? '';
6868
}
6969

70+
public function getFormattedOrigin(): string
71+
{
72+
if (!$this->getOrigin()) {
73+
return '';
74+
}
75+
76+
// symfony/translation:[email protected]/symfony/recipes:branch
77+
if (!preg_match('/^([^:]++):([^@]++)@(.+)$/', $this->getOrigin(), $matches)) {
78+
return $this->getOrigin();
79+
}
80+
81+
return sprintf('<info>%s</> (<comment>>=%s</>): From %s', $matches[1], $matches[2], 'auto-generated recipe' === $matches[3] ? '<comment>'.$matches[3].'</>' : $matches[3]);
82+
}
83+
7084
public function getURL(): string
7185
{
7286
if (!$this->data['origin']) {
@@ -99,6 +113,6 @@ public function isAuto(): bool
99113

100114
public function getVersion(): string
101115
{
102-
return $this->lock['version'];
116+
return $this->lock['recipe']['version'] ?? $this->lock['version'];
103117
}
104118
}

0 commit comments

Comments
 (0)