diff --git a/src/Configurator/CopyFromRecipeConfigurator.php b/src/Configurator/CopyFromRecipeConfigurator.php index 0e18096b..6acf6377 100644 --- a/src/Configurator/CopyFromRecipeConfigurator.php +++ b/src/Configurator/CopyFromRecipeConfigurator.php @@ -31,7 +31,13 @@ public function configure(Recipe $recipe, $config, Lock $lock, array $options = public function unconfigure(Recipe $recipe, $config, Lock $lock) { $this->write('Removing files from recipe'); - $this->removeFiles($config, $this->getRemovableFilesFromRecipeAndLock($recipe, $lock), $this->options->get('root-dir')); + $rootDir = $this->options->get('root-dir'); + + foreach ($this->options->getRemovableFiles($recipe, $lock) as $file) { + if ('.git' !== $file) { // never remove the main Git directory, even if it was created by a recipe + $this->removeFile($this->path->concatenate([$rootDir, $file])); + } + } } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void @@ -66,32 +72,6 @@ private function resolveTargetFolder(string $path, array $config): string return $path; } - private function getRemovableFilesFromRecipeAndLock(Recipe $recipe, Lock $lock): array - { - $lockedFiles = array_unique( - array_reduce( - array_column($lock->all(), 'files'), - function (array $carry, array $package) { - return array_merge($carry, $package); - }, - [] - ) - ); - - $removableFiles = $recipe->getFiles(); - - $lockedFiles = array_map('realpath', $lockedFiles); - - // Compare file paths by their real path to abstract OS differences - foreach (array_keys($removableFiles) as $file) { - if (\in_array(realpath($file), $lockedFiles)) { - unset($removableFiles[$file]); - } - } - - return $removableFiles; - } - private function copyFiles(array $manifest, array $files, array $options): array { $copiedFiles = []; @@ -148,28 +128,6 @@ private function copyFile(string $to, string $contents, bool $executable, array return $copiedFile; } - private function removeFiles(array $manifest, array $files, string $to) - { - foreach ($manifest as $source => $target) { - $target = $this->options->expandTargetDir($target); - - if ('.git' === $target) { - // never remove the main Git directory, even if it was created by a recipe - continue; - } - - if ('/' === substr($source, -1)) { - foreach (array_keys($files) as $file) { - if (str_starts_with($file, $source)) { - $this->removeFile($this->path->concatenate([$to, $target, substr($file, \strlen($source))])); - } - } - } else { - $this->removeFile($this->path->concatenate([$to, $target])); - } - } - } - private function removeFile(string $to) { if (!file_exists($to)) { diff --git a/src/Flex.php b/src/Flex.php index d1f36bf6..fdaaf003 100644 --- a/src/Flex.php +++ b/src/Flex.php @@ -111,13 +111,19 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__) $this->composer = $composer; $this->io = $io; $this->config = $composer->getConfig(); + + $composerFile = Factory::getComposerFile(); + $composerLock = 'json' === pathinfo($composerFile, \PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile.'.lock'; + $symfonyLock = str_replace('composer', 'symfony', basename($composerLock)); + + $this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: \dirname($composerLock).'/'.(basename($composerLock) !== $symfonyLock ? $symfonyLock : 'symfony.lock')); $this->options = $this->initOptions(); // if Flex is being upgraded, the original operations from the original Flex // instance are stored in the static property, so we can reuse them now. - if (property_exists(self::class, 'storedOperations') && self::$storedOperations) { - $this->operations = self::$storedOperations; - self::$storedOperations = []; + if (property_exists(Flex::class, 'storedOperations') && Flex::$storedOperations) { + $this->operations = Flex::$storedOperations; + Flex::$storedOperations = []; } $symfonyRequire = preg_replace('/\.x$/', '.x-dev', getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? '')); @@ -130,12 +136,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__) $this->filter = new PackageFilter($io, $symfonyRequire, $this->downloader); } - $composerFile = Factory::getComposerFile(); - $composerLock = 'json' === pathinfo($composerFile, \PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile.'.lock'; - $symfonyLock = str_replace('composer', 'symfony', basename($composerLock)); - $this->configurator = new Configurator($composer, $io, $this->options); - $this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: \dirname($composerLock).'/'.(basename($composerLock) !== $symfonyLock ? $symfonyLock : 'symfony.lock')); $disable = true; foreach (array_merge($composer->getPackage()->getRequires() ?? [], $composer->getPackage()->getDevRequires() ?? []) as $link) { @@ -210,8 +211,9 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__) */ public function deactivate(Composer $composer, IOInterface $io) { - // store operations in case Flex is being upgraded - self::$storedOperations = $this->operations; + // Using `Flex::` instead of `self::` to avoid issues when + // composer renames plugin classes when upgrading them + Flex::$storedOperations = $this->operations; self::$activated = false; } @@ -707,7 +709,7 @@ private function initOptions(): Options 'runtime' => $extra['runtime'] ?? [], ], $extra); - return new Options($options, $this->io); + return new Options($options, $this->io, $this->lock); } private function formatOrigin(Recipe $recipe): string diff --git a/src/Options.php b/src/Options.php index ee0bb3b3..55aeae7a 100644 --- a/src/Options.php +++ b/src/Options.php @@ -22,11 +22,13 @@ class Options private $options; private $writtenFiles = []; private $io; + private $lockData; - public function __construct(array $options = [], ?IOInterface $io = null) + public function __construct(array $options = [], ?IOInterface $io = null, ?Lock $lock = null) { $this->options = $options; $this->io = $io; + $this->lockData = $lock?->all() ?? []; } public function get(string $name) @@ -101,6 +103,38 @@ public function shouldWriteFile(string $file, bool $overwrite, bool $skipQuestio return $this->io && $this->io->askConfirmation(\sprintf('File "%s" has uncommitted changes, overwrite? [y/N] ', $name), false); } + public function getRemovableFiles(Recipe $recipe, Lock $lock): array + { + if (null === $removableFiles = $this->lockData[$recipe->getName()]['files'] ?? null) { + $removableFiles = []; + foreach (array_keys($recipe->getFiles()) as $source => $target) { + if (str_ends_with($source, '/')) { + $removableFiles[] = $this->expandTargetDir($target); + } + } + } + + unset($this->lockData[$recipe->getName()]); + $lockedFiles = array_count_values(array_merge(...array_column($lock->all(), 'files'))); + + $nonRemovableFiles = []; + foreach ($removableFiles as $i => $file) { + if (isset($lockedFiles[$file])) { + $nonRemovableFiles[] = $file; + unset($removableFiles[$i]); + } + } + + if ($nonRemovableFiles && $this->io) { + $this->io?->writeError(' The following files are still referenced by other recipes, you might need to adjust them manually:'); + foreach ($nonRemovableFiles as $file) { + $this->io?->writeError(' - '.$file); + } + } + + return array_values($removableFiles); + } + public function toArray(): array { return $this->options; diff --git a/tests/Configurator/CopyFromRecipeConfiguratorTest.php b/tests/Configurator/CopyFromRecipeConfiguratorTest.php index 95f01656..2d6ba3c5 100644 --- a/tests/Configurator/CopyFromRecipeConfiguratorTest.php +++ b/tests/Configurator/CopyFromRecipeConfiguratorTest.php @@ -61,7 +61,7 @@ public function testConfigureLocksFiles() public function testConfigureAndOverwriteFiles() { if (!file_exists($this->targetDirectory)) { - mkdir($this->targetDirectory); + @mkdir($this->targetDirectory, 0777, true); } file_put_contents($this->targetFile, '-'); $lock = $this->getMockBuilder(Lock::class)->disableOriginalConstructor()->getMock(); @@ -99,17 +99,22 @@ public function testConfigure() public function testUnconfigureKeepsLockedFiles() { if (!file_exists($this->sourceDirectory)) { - mkdir($this->sourceDirectory); + @mkdir($this->sourceDirectory, 0777, true); + } + if (!file_exists($this->targetDirectory)) { + @mkdir($this->targetDirectory, 0777, true); } + file_put_contents($this->targetFile, ''); file_put_contents($this->sourceFile, '-'); - $this->assertFileExists($this->sourceFile); $lock = new Lock(FLEX_TEST_DIR.'/test.lock'); - $lock->set('other-recipe', ['files' => ['./'.$this->targetFileRelativePath]]); + $lock->set('other-recipe', ['files' => [$this->targetFileRelativePath]]); + $this->recipe->method('getName')->willReturn('test-recipe'); $this->createConfigurator()->unconfigure($this->recipe, [$this->targetFileRelativePath], $lock); $this->assertFileExists($this->sourceFile); + $this->assertFileExists($this->targetFile); } public function testUnconfigure() @@ -118,11 +123,12 @@ public function testUnconfigure() $this->io->expects($this->at(1))->method('writeError')->with([' Removed "./config/file"']); if (!file_exists($this->targetDirectory)) { - mkdir($this->targetDirectory); + @mkdir($this->targetDirectory, 0777, true); } file_put_contents($this->targetFile, ''); $this->assertFileExists($this->targetFile); $lock = $this->getMockBuilder(Lock::class)->disableOriginalConstructor()->getMock(); + $this->recipe->method('getName')->willReturn('test-recipe'); $this->createConfigurator()->unconfigure($this->recipe, [$this->targetFileRelativePath], $lock); $this->assertFileDoesNotExist($this->targetFile); } @@ -270,8 +276,6 @@ public function testUpdateResolveDirectories() protected function setUp(): void { - parent::setUp(); - $this->sourceDirectory = FLEX_TEST_DIR.'/source'; $this->sourceFileRelativePath = 'source/file'; $this->sourceFile = $this->sourceDirectory.'/file'; @@ -294,14 +298,16 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - $this->cleanUpTargetFiles(); } private function createConfigurator(): CopyFromRecipeConfigurator { - return new CopyFromRecipeConfigurator($this->getMockBuilder(Composer::class)->getMock(), $this->io, new Options(['root-dir' => FLEX_TEST_DIR, 'config-dir' => 'config'], $this->io)); + $lock = new Lock(FLEX_TEST_DIR.'/test.lock'); + $lock->set('test-recipe', ['files' => [$this->targetFileRelativePath]]); + $options = new Options(['root-dir' => FLEX_TEST_DIR, 'config-dir' => 'config'], $this->io, $lock); + + return new CopyFromRecipeConfigurator($this->getMockBuilder(Composer::class)->getMock(), $this->io, $options); } private function cleanUpTargetFiles()