From 3a62dd9691fad8beadd6ecb5f688e39e02e49377 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 19 May 2025 16:53:40 +0200 Subject: [PATCH 1/9] Handle phpunit.xml.dist as an alias of phpunit.dist.xml --- src/Command/InstallRecipesCommand.php | 2 +- .../CopyFromPackageConfigurator.php | 4 ++-- src/Configurator/EnvConfigurator.php | 6 +++--- src/Options.php | 18 +++++++++++++++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Command/InstallRecipesCommand.php b/src/Command/InstallRecipesCommand.php index 3aa9ceeb..49992c88 100644 --- a/src/Command/InstallRecipesCommand.php +++ b/src/Command/InstallRecipesCommand.php @@ -130,7 +130,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int rename($dotenvPath, $dotenvPath.'.local'); $pipes = []; proc_close(proc_open(\sprintf('git mv %s %s > %s 2>&1 || %s %1$s %2$s', ProcessExecutor::escape($dotenvFile.'.dist'), ProcessExecutor::escape($dotenvFile), $win ? 'NUL' : '/dev/null', $win ? 'rename' : 'mv'), $pipes, $pipes, $this->rootDir)); - if (file_exists($this->rootDir.'/phpunit.xml.dist')) { + if (file_exists($this->rootDir.'/phpunit.xml.dist') || file_exists($this->rootDir.'/phpunit.dist.xml')) { touch($dotenvPath.'.test'); } } diff --git a/src/Configurator/CopyFromPackageConfigurator.php b/src/Configurator/CopyFromPackageConfigurator.php index 3ae51487..e71ff084 100644 --- a/src/Configurator/CopyFromPackageConfigurator.php +++ b/src/Configurator/CopyFromPackageConfigurator.php @@ -72,7 +72,7 @@ private function getFilesToCopy(array $manifest, string $from): array foreach ($manifest as $source => $target) { $target = $this->options->expandTargetDir($target); if ('/' === substr($source, -1)) { - $files = array_merge($files, $this->getFilesForDir($this->path->concatenate([$from, $source]), $this->path->concatenate([$target]))); + $files = array_merge($files, $this->getFilesForDir($this->path->concatenate([$from, $source]), $target)); continue; } @@ -118,7 +118,7 @@ private function getFilesForDir(string $source, string $target): array */ public function copyFile(string $source, string $target, array $options) { - $target = $this->options->get('root-dir').'/'.$target; + $target = $this->options->get('root-dir').'/'.$this->options->expandTargetDir($target); if (is_dir($source)) { // directory will be created when a file is copied to it return; diff --git a/src/Configurator/EnvConfigurator.php b/src/Configurator/EnvConfigurator.php index baddec95..339df943 100644 --- a/src/Configurator/EnvConfigurator.php +++ b/src/Configurator/EnvConfigurator.php @@ -108,7 +108,7 @@ private function configureEnvDist(Recipe $recipe, $vars, bool $update) private function configurePhpUnit(Recipe $recipe, $vars, bool $update) { - foreach (['phpunit.xml.dist', 'phpunit.xml'] as $file) { + foreach (['phpunit.xml.dist', 'phpunit.dist.xml', 'phpunit.xml'] as $file) { $phpunit = $this->options->get('root-dir').'/'.$file; if (!is_file($phpunit)) { continue; @@ -173,7 +173,7 @@ private function unconfigureEnvFiles(Recipe $recipe, $vars) private function unconfigurePhpUnit(Recipe $recipe, $vars) { - foreach (['phpunit.xml.dist', 'phpunit.xml'] as $file) { + foreach (['phpunit.dist.xml', 'phpunit.xml.dist', 'phpunit.xml'] as $file) { $phpunit = $this->options->get('root-dir').'/'.$file; if (!is_file($phpunit)) { continue; @@ -223,7 +223,7 @@ private function generateRandomBytes($length = 16) private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, array $vars): array { $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env'; - $files = '' === $this->suffix ? [$dotenvPath, $dotenvPath.'.dist', 'phpunit.xml.dist', 'phpunit.xml'] : [$dotenvPath.'.'.$this->suffix]; + $files = '' === $this->suffix ? [$dotenvPath, $dotenvPath.'.dist', 'phpunit.dist.xml', 'phpunit.xml.dist', 'phpunit.xml'] : [$dotenvPath.'.'.$this->suffix]; if (0 === \count($vars)) { return array_fill_keys($files, null); diff --git a/src/Options.php b/src/Options.php index af7b0158..96531f39 100644 --- a/src/Options.php +++ b/src/Options.php @@ -36,7 +36,7 @@ public function get(string $name) public function expandTargetDir(string $target): string { - return preg_replace_callback('{%(.+?)%}', function ($matches) { + $result = preg_replace_callback('{%(.+?)%}', function ($matches) { $option = str_replace('_', '-', strtolower($matches[1])); if (!isset($this->options[$option])) { return $matches[0]; @@ -44,6 +44,22 @@ public function expandTargetDir(string $target): string return rtrim($this->options[$option], '/'); }, $target); + + $phpunitDistFiles = [ + 'phpunit.xml.dist' => true, + 'phpunit.dist.xml' => true, + ]; + + $rootDir = $this->get('root-dir'); + + if (null === $rootDir || !isset($phpunitDistFiles[$result]) || !is_dir($rootDir) || file_exists($rootDir.'/'.$result)) { + return $result; + } + + unset($phpunitDistFiles[$result]); + $otherPhpunitDistFile = key($phpunitDistFiles); + + return file_exists($rootDir.'/'.$otherPhpunitDistFile) ? $otherPhpunitDistFile : $result; } public function shouldWriteFile(string $file, bool $overwrite): bool From ee3a0d5f1d63a44ed121f9e467868f066cea20eb Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 26 Sep 2022 15:57:57 +0200 Subject: [PATCH 2/9] Resolve directories for copy from recipe in update case --- .../CopyFromRecipeConfigurator.php | 17 ++++ tests/Command/UpdateRecipesCommandTest.php | 2 +- .../CopyFromRecipeConfiguratorTest.php | 80 ++++++++++++++++++- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/Configurator/CopyFromRecipeConfigurator.php b/src/Configurator/CopyFromRecipeConfigurator.php index 7a99a6df..47a360ea 100644 --- a/src/Configurator/CopyFromRecipeConfigurator.php +++ b/src/Configurator/CopyFromRecipeConfigurator.php @@ -37,18 +37,35 @@ public function unconfigure(Recipe $recipe, $config, Lock $lock) public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { foreach ($recipeUpdate->getOriginalRecipe()->getFiles() as $filename => $data) { + $filename = $this->resolveTargetFolder($filename, $originalConfig); $recipeUpdate->setOriginalFile($filename, $data['contents']); } $files = []; foreach ($recipeUpdate->getNewRecipe()->getFiles() as $filename => $data) { + $filename = $this->resolveTargetFolder($filename, $newConfig); $recipeUpdate->setNewFile($filename, $data['contents']); $files[] = $this->getLocalFilePath($recipeUpdate->getRootDir(), $filename); } + $recipeUpdate->getLock()->add($recipeUpdate->getPackageName(), ['files' => $files]); } + /** + * @param array $config + */ + private function resolveTargetFolder(string $path, array $config): string + { + foreach ($config as $key => $target) { + if (0 === strpos($path, $key)) { + return $this->options->expandTargetDir($target).substr($path, \strlen($key)); + } + } + + return $path; + } + private function getRemovableFilesFromRecipeAndLock(Recipe $recipe, Lock $lock): array { $lockedFiles = array_unique( diff --git a/tests/Command/UpdateRecipesCommandTest.php b/tests/Command/UpdateRecipesCommandTest.php index 1f09cf9e..efd16f54 100644 --- a/tests/Command/UpdateRecipesCommandTest.php +++ b/tests/Command/UpdateRecipesCommandTest.php @@ -100,7 +100,7 @@ private function createCommandUpdateRecipes(): CommandTester $rfs = Factory::createRemoteFilesystem($this->io, $composer->getConfig()); $rfs = new ParallelDownloader($this->io, $composer->getConfig(), $rfs->getOptions(), $rfs->isTlsDisabled()); } - $options = new Options(['root-dir' => FLEX_TEST_DIR]); + $options = new Options(['root-dir' => FLEX_TEST_DIR, 'bin-dir' => 'bin/']); $command = new UpdateRecipesCommand( $flex, new Downloader($composer, $this->io, $rfs), diff --git a/tests/Configurator/CopyFromRecipeConfiguratorTest.php b/tests/Configurator/CopyFromRecipeConfiguratorTest.php index b0e94361..95f01656 100644 --- a/tests/Configurator/CopyFromRecipeConfiguratorTest.php +++ b/tests/Configurator/CopyFromRecipeConfiguratorTest.php @@ -190,6 +190,84 @@ public function testUpdate() $this->assertSame($newRecipeFiles, $recipeUpdate->getNewFiles()); } + public function testUpdateResolveDirectories() + { + $configurator = $this->createConfigurator(); + + $lock = $this->createMock(Lock::class); + $lock->expects($this->once()) + ->method('add') + ->with( + 'test-package', + [ + 'files' => [ + 'config/packages/framework.yaml', + 'test.yaml', + ], + ] + ); + + $originalRecipeFiles = [ + 'symfony8config/packages/framework.yaml' => 'before', + 'root/test.yaml' => 'before', + ]; + $newRecipeFiles = [ + 'symfony8config/packages/framework.yaml' => 'after', + 'root/test.yaml' => 'after', + ]; + + $originalRecipeFileData = []; + foreach ($originalRecipeFiles as $file => $contents) { + $originalRecipeFileData[$file] = ['contents' => $contents, 'executable' => false]; + } + + $newRecipeFileData = []; + foreach ($newRecipeFiles as $file => $contents) { + $newRecipeFileData[$file] = ['contents' => $contents, 'executable' => false]; + } + + $originalRecipe = $this->createMock(Recipe::class); + $originalRecipe->method('getName') + ->willReturn('test-package'); + $originalRecipe->method('getFiles') + ->willReturn($originalRecipeFileData); + + $newRecipe = $this->createMock(Recipe::class); + $newRecipe->method('getFiles') + ->willReturn($newRecipeFileData); + + $recipeUpdate = new RecipeUpdate( + $originalRecipe, + $newRecipe, + $lock, + FLEX_TEST_DIR + ); + + $configurator->update( + $recipeUpdate, + [ + 'root/' => '', + 'symfony8config/' => '%CONFIG_DIR%/', + ], + [ + 'root/' => '', + 'symfony8config/' => '%CONFIG_DIR%/', + ] + ); + + // Due to root/ => '', we expect that root/ has been stripped + $this->assertArrayHasKey('test.yaml', $recipeUpdate->getOriginalFiles()); + $this->assertArrayHasKey('test.yaml', $recipeUpdate->getNewFiles()); + + $this->assertSame('after', $recipeUpdate->getNewFiles()['test.yaml']); + + // %CONFIG-DIR%, got resolved to config/packages back + $this->assertArrayHasKey('config/packages/framework.yaml', $recipeUpdate->getOriginalFiles()); + $this->assertArrayHasKey('config/packages/framework.yaml', $recipeUpdate->getNewFiles()); + + $this->assertSame('after', $recipeUpdate->getNewFiles()['config/packages/framework.yaml']); + } + protected function setUp(): void { parent::setUp(); @@ -223,7 +301,7 @@ protected function tearDown(): void private function createConfigurator(): CopyFromRecipeConfigurator { - return new CopyFromRecipeConfigurator($this->getMockBuilder(Composer::class)->getMock(), $this->io, new Options(['root-dir' => FLEX_TEST_DIR], $this->io)); + return new CopyFromRecipeConfigurator($this->getMockBuilder(Composer::class)->getMock(), $this->io, new Options(['root-dir' => FLEX_TEST_DIR, 'config-dir' => 'config'], $this->io)); } private function cleanUpTargetFiles() From 3c0362f906ad4f0551e8e0e0acc359897057950d Mon Sep 17 00:00:00 2001 From: Misha Kulakovsky Date: Tue, 20 May 2025 16:54:18 +0400 Subject: [PATCH 3/9] fix jsonPath in ComposerScriptsConfigurator::update (#1011) * fix jsonPath in ComposerScriptsConfigurator::update * fix jsonPath in ComposerScriptsConfigurator::update (+coding standards fix) * fix jsonPath in ComposerScriptsConfigurator::update * fabbot patch --- src/Configurator/ComposerScriptsConfigurator.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Configurator/ComposerScriptsConfigurator.php b/src/Configurator/ComposerScriptsConfigurator.php index abdcefc8..a230eda5 100644 --- a/src/Configurator/ComposerScriptsConfigurator.php +++ b/src/Configurator/ComposerScriptsConfigurator.php @@ -49,7 +49,11 @@ public function unconfigure(Recipe $recipe, $scripts, Lock $lock) public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $json = new JsonFile(Factory::getComposerFile()); - $jsonPath = ltrim(str_replace($recipeUpdate->getRootDir(), '', $json->getPath()), '/\\'); + $jsonPath = $json->getPath(); + if (str_starts_with($jsonPath, $recipeUpdate->getRootDir())) { + $jsonPath = substr($jsonPath, \strlen($recipeUpdate->getRootDir())); + } + $jsonPath = ltrim($jsonPath, '/\\'); $recipeUpdate->setOriginalFile( $jsonPath, From 8dfcd096625bf5854614b6b8a6c83a424213bf3b Mon Sep 17 00:00:00 2001 From: Marcin Morawski Date: Tue, 20 May 2025 14:57:15 +0200 Subject: [PATCH 4/9] Real composer scripts (#930) --- src/Configurator.php | 1 + .../ComposerCommandsConfigurator.php | 73 +++++ .../ComposerCommandConfiguratorTest.php | 287 ++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 src/Configurator/ComposerCommandsConfigurator.php create mode 100644 tests/Configurator/ComposerCommandConfiguratorTest.php diff --git a/src/Configurator.php b/src/Configurator.php index dbffb109..b5f822a8 100644 --- a/src/Configurator.php +++ b/src/Configurator.php @@ -43,6 +43,7 @@ public function __construct(Composer $composer, IOInterface $io, Options $option 'container' => Configurator\ContainerConfigurator::class, 'makefile' => Configurator\MakefileConfigurator::class, 'composer-scripts' => Configurator\ComposerScriptsConfigurator::class, + 'composer-commands' => Configurator\ComposerCommandsConfigurator::class, 'gitignore' => Configurator\GitignoreConfigurator::class, 'dockerfile' => Configurator\DockerfileConfigurator::class, 'docker-compose' => Configurator\DockerComposeConfigurator::class, diff --git a/src/Configurator/ComposerCommandsConfigurator.php b/src/Configurator/ComposerCommandsConfigurator.php new file mode 100644 index 00000000..5e29e2e1 --- /dev/null +++ b/src/Configurator/ComposerCommandsConfigurator.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Flex\Configurator; + +use Composer\Factory; +use Composer\Json\JsonFile; +use Composer\Json\JsonManipulator; +use Symfony\Flex\Lock; +use Symfony\Flex\Recipe; +use Symfony\Flex\Update\RecipeUpdate; + +/** + * @author Marcin Morawski + */ +class ComposerCommandsConfigurator extends AbstractConfigurator +{ + public function configure(Recipe $recipe, $scripts, Lock $lock, array $options = []) + { + $json = new JsonFile(Factory::getComposerFile()); + + file_put_contents($json->getPath(), $this->configureScripts($scripts, $json)); + } + + public function unconfigure(Recipe $recipe, $scripts, Lock $lock) + { + $json = new JsonFile(Factory::getComposerFile()); + + $manipulator = new JsonManipulator(file_get_contents($json->getPath())); + foreach ($scripts as $key => $command) { + $manipulator->removeSubNode('scripts', $key); + } + + file_put_contents($json->getPath(), $manipulator->getContents()); + } + + public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void + { + $json = new JsonFile(Factory::getComposerFile()); + $jsonPath = $json->getPath(); + if (str_starts_with($jsonPath, $recipeUpdate->getRootDir())) { + $jsonPath = substr($jsonPath, \strlen($recipeUpdate->getRootDir())); + } + $jsonPath = ltrim($jsonPath, '/\\'); + + $recipeUpdate->setOriginalFile( + $jsonPath, + $this->configureScripts($originalConfig, $json) + ); + $recipeUpdate->setNewFile( + $jsonPath, + $this->configureScripts($newConfig, $json) + ); + } + + private function configureScripts(array $scripts, JsonFile $json): string + { + $manipulator = new JsonManipulator(file_get_contents($json->getPath())); + foreach ($scripts as $cmdName => $script) { + $manipulator->addSubNode('scripts', $cmdName, $script); + } + + return $manipulator->getContents(); + } +} diff --git a/tests/Configurator/ComposerCommandConfiguratorTest.php b/tests/Configurator/ComposerCommandConfiguratorTest.php new file mode 100644 index 00000000..23a72df6 --- /dev/null +++ b/tests/Configurator/ComposerCommandConfiguratorTest.php @@ -0,0 +1,287 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Flex\Tests\Configurator; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\Util\Platform; +use PHPUnit\Framework\TestCase; +use Symfony\Flex\Configurator\ComposerCommandsConfigurator; +use Symfony\Flex\Lock; +use Symfony\Flex\Options; +use Symfony\Flex\Recipe; +use Symfony\Flex\Update\RecipeUpdate; + +class ComposerCommandConfiguratorTest extends TestCase +{ + protected function setUp(): void + { + @mkdir(FLEX_TEST_DIR); + if (method_exists(Platform::class, 'putEnv')) { + Platform::putEnv('COMPOSER', FLEX_TEST_DIR.'/composer.json'); + } else { + putenv('COMPOSER='.FLEX_TEST_DIR.'/composer.json'); + } + } + + protected function tearDown(): void + { + @unlink(FLEX_TEST_DIR.'/composer.json'); + @rmdir(FLEX_TEST_DIR); + if (method_exists(Platform::class, 'clearEnv')) { + Platform::clearEnv('COMPOSER'); + } else { + putenv('COMPOSER'); + } + } + + /** + * @dataProvider providerForConfigureMethod + */ + public function testConfigure($composerSchema, string $expectedComposerJson): void + { + file_put_contents(FLEX_TEST_DIR.'/composer.json', json_encode($composerSchema, \JSON_PRETTY_PRINT)); + + $configurator = new ComposerCommandsConfigurator( + $this->createMock(Composer::class), + $this->createMock(IOInterface::class), + new Options(['root-dir' => FLEX_TEST_DIR]) + ); + + $recipe = $this->getMockBuilder(Recipe::class)->disableOriginalConstructor()->getMock(); + $lock = $this->getMockBuilder(Lock::class)->disableOriginalConstructor()->getMock(); + + $configurator->configure($recipe, [ + 'do:cool-stuff' => 'symfony-cmd', + ], $lock); + $this->assertEquals( + $expectedComposerJson, + file_get_contents(FLEX_TEST_DIR.'/composer.json') + ); + } + + public static function providerForConfigureMethod(): iterable + { + yield 'without_scripts_block' => [ + new \stdClass(), + << [ + [ + 'scripts' => [ + 'foo' => 'bar', + ], + ], + << [ + [ + 'scripts' => [ + 'auto-scripts' => [ + 'cache:clear' => 'symfony-cmd', + 'assets:install %PUBLIC_DIR%' => 'symfony-cmd', + ], + 'post-install-cmd' => ['@auto-scripts'], + 'post-update-cmd' => ['@auto-scripts'], + ], + ], + <<createMock(Composer::class), + $this->createMock(IOInterface::class), + new Options(['root-dir' => FLEX_TEST_DIR]) + ); + + $recipe = $this->createMock(Recipe::class); + $lock = $this->createMock(Lock::class); + + $configurator->unconfigure($recipe, [ + 'do:cool-stuff' => 'symfony-cmd', + ], $lock); + $this->assertEquals( + $expectedComposerJson, + file_get_contents(FLEX_TEST_DIR.'/composer.json') + ); + } + + public static function providerForUnconfigureMethod(): iterable + { + yield 'unconfigure_one_command_with_auto_scripts' => [ + [ + 'scripts' => [ + 'auto-scripts' => [ + 'cache:clear' => 'symfony-cmd', + 'assets:install %PUBLIC_DIR%' => 'symfony-cmd', + ], + 'post-install-cmd' => ['@auto-scripts'], + 'post-update-cmd' => ['@auto-scripts'], + 'do:cool-stuff' => 'symfony-cmd', + 'do:another-cool-stuff' => 'symfony-cmd-2', + ], + ], + << [ + [ + 'scripts' => [ + 'do:another-cool-stuff' => 'symfony-cmd-2', + 'do:cool-stuff' => 'symfony-cmd', + ], + ], + <<createMock(Composer::class), + $this->createMock(IOInterface::class), + new Options(['root-dir' => FLEX_TEST_DIR]) + ); + + $recipeUpdate = new RecipeUpdate( + $this->createMock(Recipe::class), + $this->createMock(Recipe::class), + $this->createMock(Lock::class), + FLEX_TEST_DIR + ); + + file_put_contents(FLEX_TEST_DIR.'/composer.json', json_encode([ + 'scripts' => [ + 'auto-scripts' => [ + 'cache:clear' => 'symfony-cmd', + 'assets:install %PUBLIC_DIR%' => 'symfony-cmd', + ], + 'post-install-cmd' => ['@auto-scripts'], + 'post-update-cmd' => ['@auto-scripts'], + 'foo' => 'bar', + ], + ], \JSON_PRETTY_PRINT)); + + $configurator->update( + $recipeUpdate, + ['foo' => 'bar'], + ['foo' => 'baz', 'do:cool-stuff' => 'symfony-cmd'] + ); + + $expectedComposerJsonOriginal = <<assertSame(['composer.json' => $expectedComposerJsonOriginal], $recipeUpdate->getOriginalFiles()); + + $expectedComposerJsonNew = <<assertSame(['composer.json' => $expectedComposerJsonNew], $recipeUpdate->getNewFiles()); + } +} From 9865fc19eb88d9091176c7150d8fecd5bd116474 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 20 May 2025 15:10:05 +0200 Subject: [PATCH 5/9] Dont run git hooks when running recipes:update --- src/Update/RecipePatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Update/RecipePatcher.php b/src/Update/RecipePatcher.php index 69eb7c1d..69773a06 100644 --- a/src/Update/RecipePatcher.php +++ b/src/Update/RecipePatcher.php @@ -98,7 +98,7 @@ public function generatePatch(array $originalFiles, array $newFiles): RecipePatc if (\count($originalFiles) > 0) { $this->writeFiles($originalFiles, $tmpPath); $this->execute('git add -A', $tmpPath); - $this->execute('git commit -m "original files"', $tmpPath); + $this->execute('git commit -n -m "original files"', $tmpPath); $blobs = $this->generateBlobs($originalFiles, $tmpPath); } From e60e03dc6259847d34a4ac2234ea25460cc5819c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 20 May 2025 15:22:16 +0200 Subject: [PATCH 6/9] Ignore warnings related to opcache.restrict_api --- src/Configurator/BundlesConfigurator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Configurator/BundlesConfigurator.php b/src/Configurator/BundlesConfigurator.php index 2aab5e74..799cc654 100644 --- a/src/Configurator/BundlesConfigurator.php +++ b/src/Configurator/BundlesConfigurator.php @@ -118,7 +118,7 @@ private function dump(string $file, array $bundles) file_put_contents($file, $contents); if (\function_exists('opcache_invalidate')) { - opcache_invalidate($file); + @opcache_invalidate($file); } } From 9c701619674add1d2f3282b67dbfcc9e9bd03164 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Tue, 20 May 2025 16:07:00 +0200 Subject: [PATCH 7/9] Add --yes flag to recipes:install command to overwrite all files without asking --- src/Command/InstallRecipesCommand.php | 3 +- .../CopyFromPackageConfigurator.php | 3 +- .../CopyFromRecipeConfigurator.php | 3 +- src/Event/UpdateEvent.php | 9 +++- src/Flex.php | 2 + src/Options.php | 6 ++- tests/Command/InstallRecipesCommandTest.php | 42 ++++++++++++++++ tests/OptionsTest.php | 49 +++++++++++++++++++ 8 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 tests/Command/InstallRecipesCommandTest.php create mode 100644 tests/OptionsTest.php diff --git a/src/Command/InstallRecipesCommand.php b/src/Command/InstallRecipesCommand.php index 49992c88..1f43ece7 100644 --- a/src/Command/InstallRecipesCommand.php +++ b/src/Command/InstallRecipesCommand.php @@ -46,6 +46,7 @@ protected function configure() ->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Recipes that should be installed.') ->addOption('force', null, InputOption::VALUE_NONE, 'Overwrite existing files when a new version of a recipe is available') ->addOption('reset', null, InputOption::VALUE_NONE, 'Reset all recipes back to their initial state (should be combined with --force)') + ->addOption('yes', null, InputOption::VALUE_NONE, "Answer prompt questions with 'yes' for all questions.") ; } @@ -135,7 +136,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - $this->flex->update(new UpdateEvent($force, (bool) $input->getOption('reset')), $operations); + $this->flex->update(new UpdateEvent($force, (bool) $input->getOption('reset'), (bool) $input->getOption('yes')), $operations); if ($force) { $output = [ diff --git a/src/Configurator/CopyFromPackageConfigurator.php b/src/Configurator/CopyFromPackageConfigurator.php index e71ff084..f30355ee 100644 --- a/src/Configurator/CopyFromPackageConfigurator.php +++ b/src/Configurator/CopyFromPackageConfigurator.php @@ -124,8 +124,7 @@ public function copyFile(string $source, string $target, array $options) return; } - $overwrite = $options['force'] ?? false; - if (!$this->options->shouldWriteFile($target, $overwrite)) { + if (!$this->options->shouldWriteFile($target, $options['force'] ?? false, $options['assumeYesForPrompts'] ?? false)) { return; } diff --git a/src/Configurator/CopyFromRecipeConfigurator.php b/src/Configurator/CopyFromRecipeConfigurator.php index 47a360ea..004c0653 100644 --- a/src/Configurator/CopyFromRecipeConfigurator.php +++ b/src/Configurator/CopyFromRecipeConfigurator.php @@ -127,11 +127,10 @@ private function copyDir(string $source, string $target, array $files, array $op private function copyFile(string $to, string $contents, bool $executable, array $options): string { - $overwrite = $options['force'] ?? false; $basePath = $options['root-dir'] ?? '.'; $copiedFile = $this->getLocalFilePath($basePath, $to); - if (!$this->options->shouldWriteFile($to, $overwrite)) { + if (!$this->options->shouldWriteFile($to, $options['force'] ?? false, $options['assumeYesForPrompts'] ?? false)) { return $copiedFile; } diff --git a/src/Event/UpdateEvent.php b/src/Event/UpdateEvent.php index 06dbe0c5..68f7547c 100644 --- a/src/Event/UpdateEvent.php +++ b/src/Event/UpdateEvent.php @@ -18,12 +18,14 @@ class UpdateEvent extends Event { private $force; private $reset; + private $assumeYesForPrompts; - public function __construct(bool $force, bool $reset) + public function __construct(bool $force, bool $reset, bool $assumeYesForPrompts) { $this->name = ScriptEvents::POST_UPDATE_CMD; $this->force = $force; $this->reset = $reset; + $this->assumeYesForPrompts = $assumeYesForPrompts; } public function force(): bool @@ -35,4 +37,9 @@ public function reset(): bool { return $this->reset; } + + public function assumeYesForPrompts(): bool + { + return $this->assumeYesForPrompts; + } } diff --git a/src/Flex.php b/src/Flex.php index 995a45ee..b7d10209 100644 --- a/src/Flex.php +++ b/src/Flex.php @@ -447,6 +447,7 @@ function ($value) { $this->io->writeError(\sprintf(' - Configuring %s', $this->formatOrigin($recipe))); $this->configurator->install($recipe, $this->lock, [ 'force' => $event instanceof UpdateEvent && $event->force(), + 'assumeYesForPrompts' => $event instanceof UpdateEvent && $event->assumeYesForPrompts(), ]); $manifest = $recipe->getManifest(); if (isset($manifest['post-install-output'])) { @@ -471,6 +472,7 @@ function ($value) { foreach ($postInstallRecipes as $recipe) { $this->configurator->postInstall($recipe, $this->lock, [ 'force' => $event instanceof UpdateEvent && $event->force(), + 'assumeYesForPrompts' => $event instanceof UpdateEvent && $event->assumeYesForPrompts(), ]); } } diff --git a/src/Options.php b/src/Options.php index 96531f39..ee0bb3b3 100644 --- a/src/Options.php +++ b/src/Options.php @@ -62,7 +62,7 @@ public function expandTargetDir(string $target): string return file_exists($rootDir.'/'.$otherPhpunitDistFile) ? $otherPhpunitDistFile : $result; } - public function shouldWriteFile(string $file, bool $overwrite): bool + public function shouldWriteFile(string $file, bool $overwrite, bool $skipQuestion): bool { if (isset($this->writtenFiles[$file])) { return false; @@ -81,6 +81,10 @@ public function shouldWriteFile(string $file, bool $overwrite): bool return true; } + if ($skipQuestion) { + return true; + } + exec('git status --short --ignored --untracked-files=all -- '.ProcessExecutor::escape($file).' 2>&1', $output, $status); if (0 !== $status) { diff --git a/tests/Command/InstallRecipesCommandTest.php b/tests/Command/InstallRecipesCommandTest.php new file mode 100644 index 00000000..46216af1 --- /dev/null +++ b/tests/Command/InstallRecipesCommandTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Flex\Tests\Command; + +use Composer\Console\Application; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Flex\Command\InstallRecipesCommand; +use Symfony\Flex\Event\UpdateEvent; +use Symfony\Flex\Flex; + +class InstallRecipesCommandTest extends TestCase +{ + public function testCommandFlagsPassedDown() + { + $flex = $this->createMock(Flex::class); + $flex->method('update')->willReturnCallback(function (UpdateEvent $event) { + $this->assertTrue($event->reset()); + $this->assertTrue($event->assumeYesForPrompts()); + }); + + $command = new InstallRecipesCommand($flex, __DIR__); + $application = new Application(); + $application->add($command); + $command = $application->find('symfony:recipes:install'); + + $tester = new CommandTester($command); + $tester->execute([ + '--reset' => true, + '--yes' => true, + ]); + } +} diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php new file mode 100644 index 00000000..1064fcbf --- /dev/null +++ b/tests/OptionsTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Flex\Tests; + +use Composer\IO\IOInterface; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; +use Symfony\Flex\Options; + +class OptionsTest extends TestCase +{ + public function testShouldWrite() + { + @mkdir(FLEX_TEST_DIR); + (new Process(['git', 'init'], FLEX_TEST_DIR))->mustRun(); + (new Process(['git', 'config', 'user.name', 'Unit test'], FLEX_TEST_DIR))->mustRun(); + (new Process(['git', 'config', 'user.email', ''], FLEX_TEST_DIR))->mustRun(); + + $filePath = FLEX_TEST_DIR.'/a.txt'; + file_put_contents($filePath, 'a'); + (new Process(['git', 'add', '-A'], FLEX_TEST_DIR))->mustRun(); + (new Process(['git', 'commit', '-m', 'setup of original files'], FLEX_TEST_DIR))->mustRun(); + + file_put_contents($filePath, 'b'); + + $this->assertTrue((new Options([], null))->shouldWriteFile('non-existing-file.txt', false, false)); + $this->assertFalse((new Options([], null))->shouldWriteFile($filePath, false, false)); + + // We don't have an IO, so we don't write the file + $this->assertFalse((new Options([], null))->shouldWriteFile($filePath, true, false)); + + // We have an IO, and it allowed to write the file + $io = $this->createMock(IOInterface::class); + $io->expects($this->once())->method('askConfirmation')->willReturn(true); + $this->assertTrue((new Options([], $io))->shouldWriteFile($filePath, true, false)); + + // We skip all questions, so we're able to write + $this->assertTrue((new Options([], null))->shouldWriteFile($filePath, true, true)); + } +} From cfe894ce89b45adcf306c13f402c11a713c281f9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 21 May 2025 09:04:52 +0200 Subject: [PATCH 8/9] Fix needless call to flex API on composer i --- src/PackageFilter.php | 6 +++--- tests/PackageFilterTest.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PackageFilter.php b/src/PackageFilter.php index 8de0b500..96144b68 100644 --- a/src/PackageFilter.php +++ b/src/PackageFilter.php @@ -65,7 +65,7 @@ public function removeLegacyPackages(array $data, RootPackageInterface $rootPack $rootConstraints[$name] = $link->getConstraint(); } - $knownVersions = $this->getVersions(); + $knownVersions = null; $filteredPackages = []; $symfonyPackages = []; $oneSymfony = false; @@ -77,8 +77,8 @@ public function removeLegacyPackages(array $data, RootPackageInterface $rootPack } if ('symfony/symfony' !== $name && ( - !isset($knownVersions['splits'][$name]) - || array_intersect($versions, $lockedVersions[$name] ?? []) + array_intersect($versions, $lockedVersions[$name] ?? []) + || (($knownVersions ??= $this->getVersions()) && !isset($knownVersions['splits'][$name])) || (isset($rootConstraints[$name]) && !Intervals::haveIntersections($this->symfonyConstraints, $rootConstraints[$name])) || ('symfony/psr-http-message-bridge' === $name && 6.4 > $versions[0]) )) { diff --git a/tests/PackageFilterTest.php b/tests/PackageFilterTest.php index 0b7292e8..5793c6bf 100644 --- a/tests/PackageFilterTest.php +++ b/tests/PackageFilterTest.php @@ -32,7 +32,7 @@ class PackageFilterTest extends TestCase public function testRemoveLegacyPackages(array $expected, array $packages, string $symfonyRequire, array $versions, array $lockedPackages = []) { $downloader = $this->getMockBuilder('Symfony\Flex\Downloader')->disableOriginalConstructor()->getMock(); - $downloader->expects($this->once()) + $downloader->expects($this->any()) ->method('getVersions') ->willReturn($versions); $filter = new PackageFilter(new NullIO(), $symfonyRequire, $downloader); From 39ff4d5f377f63ea3c368ee94aaee024920a5b23 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 21 May 2025 09:19:03 +0200 Subject: [PATCH 9/9] Dont copy constraint from framework-bundle when installing a new symfony package --- src/PackageResolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PackageResolver.php b/src/PackageResolver.php index 3e25584a..ddb054d2 100644 --- a/src/PackageResolver.php +++ b/src/PackageResolver.php @@ -68,11 +68,11 @@ public function parseVersion(string $package, string $version, bool $isRequire): try { $config = @json_decode(file_get_contents(Factory::getComposerFile()), true); } finally { - if (!$isRequire || !(isset($config['extra']['symfony']['require']) || isset($config['require']['symfony/framework-bundle']))) { + if (!$isRequire || !isset($config['extra']['symfony']['require'])) { return ''; } } - $version = $config['extra']['symfony']['require'] ?? $config['require']['symfony/framework-bundle']; + $version = $config['extra']['symfony']['require']; } elseif ('dev' === $version) { $version = '^'.$versions['dev-name'].'@dev'; } elseif ('next' === $version) {