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()