Skip to content

Commit 7f04fb5

Browse files
bug #451 Lock copied files to prevent removing files needed by other recipes (dbrumann)
This PR was merged into the 1.2-dev branch. Discussion ---------- Lock copied files to prevent removing files needed by other recipes This PR modifies the `CopyFromRecipeConfigurator` so that it will store a list of copied files in symfony.lock and when a recipe is about to be unconfigured, it will check the list of locked files to prevent removal. This should fix #428 ## How to reproduce the issue ``` composer create-project symfony/skeleton example cd example composer remove console ``` This will remove both files installed through the recipe `bin/console` and `config/bootstrap.php`. As `symfony/framework-bundle` relies on the bootstrap-file as well, when trying to access the website, you will get an error due to the missing file. ## How to verify the fix Create a new project based on `symfony/skeleton` and replace flex inside the example project with a local copy of this branch: ``` composer create-project symfony/skeleton example cd example rm -rf vendor/symfony/flex ln -s path/to/local/flex vendor/symfony/flex ``` Since the `symfony.lock` was created during setup of the project it will not yet keep track of the copied files and needs to be updated first. An easy way to do this, is to remove the current file and run `composer fix-recipes` to create a new one: ``` rm symfony.lock composer fix-recipes ``` Alternatively removing a recipe and then requiring it again will update the lock as well, but it needs to be done for each recipe, as otherwise the list of locked files would be incomplete. Now when we remove the console again, opening the page in the browser should still work and the file `config/bootstrap.php` should still exist as it is locked by the framework-bundle. ## BC Breaks None, as far as I can tell. For newly required recipes the files will be tracked. The new key for files in `symfony.lock` was not previously used and should not interfere with existing behavior. ## Known limitations Unfortunately if any of the previously installed recipes has conflicting files, this will not be recognized unless the recipe is removed and then installed again. Commits ------- 896e709 Adds file locking for CopyFromRecipe.
2 parents 87d511b + 896e709 commit 7f04fb5

21 files changed

+225
-84
lines changed

src/Configurator.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,22 @@ public function __construct(Composer $composer, IOInterface $io, Options $option
4444
];
4545
}
4646

47-
public function install(Recipe $recipe, array $options = [])
47+
public function install(Recipe $recipe, Lock $lock, array $options = [])
4848
{
4949
$manifest = $recipe->getManifest();
5050
foreach (array_keys($this->configurators) as $key) {
5151
if (isset($manifest[$key])) {
52-
$this->get($key)->configure($recipe, $manifest[$key], $options);
52+
$this->get($key)->configure($recipe, $manifest[$key], $lock, $options);
5353
}
5454
}
5555
}
5656

57-
public function unconfigure(Recipe $recipe)
57+
public function unconfigure(Recipe $recipe, Lock $lock)
5858
{
5959
$manifest = $recipe->getManifest();
6060
foreach (array_keys($this->configurators) as $key) {
6161
if (isset($manifest[$key])) {
62-
$this->get($key)->unconfigure($recipe, $manifest[$key]);
62+
$this->get($key)->unconfigure($recipe, $manifest[$key], $lock);
6363
}
6464
}
6565
}

src/Configurator/AbstractConfigurator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Composer\Composer;
1515
use Composer\IO\IOInterface;
16+
use Symfony\Flex\Lock;
1617
use Symfony\Flex\Options;
1718
use Symfony\Flex\Path;
1819
use Symfony\Flex\Recipe;
@@ -35,9 +36,9 @@ public function __construct(Composer $composer, IOInterface $io, Options $option
3536
$this->path = new Path($options->get('root-dir'));
3637
}
3738

38-
abstract public function configure(Recipe $recipe, $config, array $options = []);
39+
abstract public function configure(Recipe $recipe, $config, Lock $lock, array $options = []);
3940

40-
abstract public function unconfigure(Recipe $recipe, $config);
41+
abstract public function unconfigure(Recipe $recipe, $config, Lock $lock);
4142

4243
protected function write($messages)
4344
{

src/Configurator/BundlesConfigurator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111

1212
namespace Symfony\Flex\Configurator;
1313

14+
use Symfony\Flex\Lock;
1415
use Symfony\Flex\Recipe;
1516

1617
/**
1718
* @author Fabien Potencier <[email protected]>
1819
*/
1920
class BundlesConfigurator extends AbstractConfigurator
2021
{
21-
public function configure(Recipe $recipe, $bundles, array $options = [])
22+
public function configure(Recipe $recipe, $bundles, Lock $lock, array $options = [])
2223
{
2324
$this->write('Enabling the package as a Symfony bundle');
2425
$file = $this->getConfFile();
@@ -38,7 +39,7 @@ public function configure(Recipe $recipe, $bundles, array $options = [])
3839
$this->dump($file, $registered);
3940
}
4041

41-
public function unconfigure(Recipe $recipe, $bundles)
42+
public function unconfigure(Recipe $recipe, $bundles, Lock $lock)
4243
{
4344
$this->write('Disabling the Symfony bundle');
4445
$file = $this->getConfFile();

src/Configurator/ComposerScriptsConfigurator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
use Composer\Factory;
1515
use Composer\Json\JsonFile;
1616
use Composer\Json\JsonManipulator;
17+
use Symfony\Flex\Lock;
1718
use Symfony\Flex\Recipe;
1819

1920
/**
2021
* @author Fabien Potencier <[email protected]>
2122
*/
2223
class ComposerScriptsConfigurator extends AbstractConfigurator
2324
{
24-
public function configure(Recipe $recipe, $scripts, array $options = [])
25+
public function configure(Recipe $recipe, $scripts, Lock $lock, array $options = [])
2526
{
2627
$json = new JsonFile(Factory::getComposerFile());
2728

@@ -35,7 +36,7 @@ public function configure(Recipe $recipe, $scripts, array $options = [])
3536
file_put_contents($json->getPath(), $manipulator->getContents());
3637
}
3738

38-
public function unconfigure(Recipe $recipe, $scripts)
39+
public function unconfigure(Recipe $recipe, $scripts, Lock $lock)
3940
{
4041
$json = new JsonFile(Factory::getComposerFile());
4142

src/Configurator/ContainerConfigurator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@
1111

1212
namespace Symfony\Flex\Configurator;
1313

14+
use Symfony\Flex\Lock;
1415
use Symfony\Flex\Recipe;
1516

1617
/**
1718
* @author Fabien Potencier <[email protected]>
1819
*/
1920
class ContainerConfigurator extends AbstractConfigurator
2021
{
21-
public function configure(Recipe $recipe, $parameters, array $options = [])
22+
public function configure(Recipe $recipe, $parameters, Lock $lock, array $options = [])
2223
{
2324
$this->write('Setting parameters');
2425
$this->addParameters($parameters);
2526
}
2627

27-
public function unconfigure(Recipe $recipe, $parameters)
28+
public function unconfigure(Recipe $recipe, $parameters, Lock $lock)
2829
{
2930
$this->write('Unsetting parameters');
3031
$target = $this->options->get('root-dir').'/'.$this->options->expandTargetDir('%CONFIG_DIR%/services.yaml');

src/Configurator/CopyFromPackageConfigurator.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,45 @@
1212
namespace Symfony\Flex\Configurator;
1313

1414
use LogicException;
15+
use Symfony\Flex\Lock;
1516
use Symfony\Flex\Recipe;
1617

1718
/**
1819
* @author Fabien Potencier <[email protected]>
1920
*/
2021
class CopyFromPackageConfigurator extends AbstractConfigurator
2122
{
22-
public function configure(Recipe $recipe, $config, array $options = [])
23+
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
2324
{
2425
$this->write('Setting configuration and copying files');
2526
$packageDir = $this->composer->getInstallationManager()->getInstallPath($recipe->getPackage());
26-
$this->copyFiles($config, $packageDir, $this->options->get('root-dir'), $options['force'] ?? false);
27+
$options = array_merge($this->options->toArray(), $options);
28+
29+
$this->copyFiles($config, $packageDir, $options);
2730
}
2831

29-
public function unconfigure(Recipe $recipe, $config)
32+
public function unconfigure(Recipe $recipe, $config, Lock $lock)
3033
{
3134
$this->write('Removing configuration and files');
3235
$packageDir = $this->composer->getInstallationManager()->getInstallPath($recipe->getPackage());
3336
$this->removeFiles($config, $packageDir, $this->options->get('root-dir'));
3437
}
3538

36-
private function copyFiles(array $manifest, string $from, string $to, bool $overwrite = false)
39+
private function copyFiles(array $manifest, string $from, array $options)
3740
{
41+
$to = $options['root-dir'] ?? '.';
3842
foreach ($manifest as $source => $target) {
3943
$target = $this->options->expandTargetDir($target);
4044
if ('/' === substr($source, -1)) {
41-
$this->copyDir($this->path->concatenate([$from, $source]), $this->path->concatenate([$to, $target]), $overwrite);
45+
$this->copyDir($this->path->concatenate([$from, $source]), $this->path->concatenate([$to, $target]), $options);
4246
} else {
4347
$targetPath = $this->path->concatenate([$to, $target]);
4448
if (!is_dir(\dirname($targetPath))) {
4549
mkdir(\dirname($targetPath), 0777, true);
4650
$this->write(sprintf('Created <fg=green>"%s"</>', $this->path->relativize(\dirname($targetPath))));
4751
}
4852

49-
$this->copyFile($this->path->concatenate([$from, $source]), $targetPath, $overwrite);
53+
$this->copyFile($this->path->concatenate([$from, $source]), $targetPath, $options);
5054
}
5155
}
5256
}
@@ -67,7 +71,7 @@ private function removeFiles(array $manifest, string $from, string $to)
6771
}
6872
}
6973

70-
private function copyDir(string $source, string $target, bool $overwrite)
74+
private function copyDir(string $source, string $target, array $options)
7175
{
7276
if (!is_dir($target)) {
7377
mkdir($target, 0777, true);
@@ -82,13 +86,14 @@ private function copyDir(string $source, string $target, bool $overwrite)
8286
$this->write(sprintf('Created <fg=green>"%s"</>', $this->path->relativize($targetPath)));
8387
}
8488
} elseif (!file_exists($targetPath)) {
85-
$this->copyFile($item, $targetPath, $overwrite);
89+
$this->copyFile($item, $targetPath, $options);
8690
}
8791
}
8892
}
8993

90-
public function copyFile(string $source, string $target, bool $overwrite = false)
94+
public function copyFile(string $source, string $target, array $options)
9195
{
96+
$overwrite = $options['force'] ?? false;
9297
if (!$this->options->shouldWriteFile($target, $overwrite)) {
9398
return;
9499
}

src/Configurator/CopyFromRecipeConfigurator.php

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,51 +11,91 @@
1111

1212
namespace Symfony\Flex\Configurator;
1313

14+
use Symfony\Flex\Lock;
1415
use Symfony\Flex\Recipe;
1516

1617
/**
1718
* @author Fabien Potencier <[email protected]>
1819
*/
1920
class CopyFromRecipeConfigurator extends AbstractConfigurator
2021
{
21-
public function configure(Recipe $recipe, $config, array $options = [])
22+
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
2223
{
2324
$this->write('Setting configuration and copying files');
24-
$this->copyFiles($config, $recipe->getFiles(), $this->options->get('root-dir'), $options['force'] ?? false);
25+
$options = array_merge($this->options->toArray(), $options);
26+
27+
$lock->add($recipe->getName(), ['files' => $this->copyFiles($config, $recipe->getFiles(), $options)]);
2528
}
2629

27-
public function unconfigure(Recipe $recipe, $config)
30+
public function unconfigure(Recipe $recipe, $config, Lock $lock)
2831
{
2932
$this->write('Removing configuration and files');
30-
$this->removeFiles($config, $recipe->getFiles(), $this->options->get('root-dir'));
33+
$this->removeFiles($config, $this->getRemovableFilesFromRecipeAndLock($recipe, $lock), $this->options->get('root-dir'));
34+
}
35+
36+
private function getRemovableFilesFromRecipeAndLock(Recipe $recipe, Lock $lock): array
37+
{
38+
$lockedFiles = array_unique(
39+
array_reduce(
40+
array_column($lock->all(), 'files'),
41+
function (array $carry, array $package) {
42+
return array_merge($carry, $package);
43+
},
44+
[]
45+
)
46+
);
47+
48+
$removableFiles = $recipe->getFiles();
49+
foreach ($lockedFiles as $file) {
50+
if (isset($removableFiles[$file])) {
51+
unset($removableFiles[$file]);
52+
}
53+
}
54+
55+
return $removableFiles;
3156
}
3257

33-
private function copyFiles(array $manifest, array $files, string $to, bool $overwrite = false)
58+
private function copyFiles(array $manifest, array $files, array $options): array
3459
{
60+
$copiedFiles = [];
61+
$to = $options['root-dir'] ?? '.';
62+
3563
foreach ($manifest as $source => $target) {
3664
$target = $this->options->expandTargetDir($target);
3765
if ('/' === substr($source, -1)) {
38-
$this->copyDir($source, $this->path->concatenate([$to, $target]), $files, $overwrite);
66+
$copiedFiles = array_merge(
67+
$copiedFiles,
68+
$this->copyDir($source, $this->path->concatenate([$to, $target]), $files, $options)
69+
);
3970
} else {
40-
$this->copyFile($this->path->concatenate([$to, $target]), $files[$source]['contents'], $files[$source]['executable'], $overwrite);
71+
$copiedFiles[] = $this->copyFile($this->path->concatenate([$to, $target]), $files[$source]['contents'], $files[$source]['executable'], $options);
4172
}
4273
}
74+
75+
return $copiedFiles;
4376
}
4477

45-
private function copyDir(string $source, string $target, array $files, bool $overwrite = false)
78+
private function copyDir(string $source, string $target, array $files, array $options): array
4679
{
80+
$copiedFiles = [];
4781
foreach ($files as $file => $data) {
4882
if (0 === strpos($file, $source)) {
4983
$file = $this->path->concatenate([$target, substr($file, \strlen($source))]);
50-
$this->copyFile($file, $data['contents'], $data['executable'], $overwrite);
84+
$copiedFiles[] = $this->copyFile($file, $data['contents'], $data['executable'], $options);
5185
}
5286
}
87+
88+
return $copiedFiles;
5389
}
5490

55-
private function copyFile(string $to, string $contents, bool $executable, bool $overwrite = false)
91+
private function copyFile(string $to, string $contents, bool $executable, array $options): string
5692
{
93+
$overwrite = $options['force'] ?? false;
94+
$basePath = $options['root-dir'] ?? '.';
95+
$copiedFile = str_replace($basePath.\DIRECTORY_SEPARATOR, '', $to);
96+
5797
if (!$this->options->shouldWriteFile($to, $overwrite)) {
58-
return;
98+
return $copiedFile;
5999
}
60100

61101
if (!is_dir(\dirname($to))) {
@@ -68,6 +108,8 @@ private function copyFile(string $to, string $contents, bool $executable, bool $
68108
}
69109

70110
$this->write(sprintf('Created <fg=green>"%s"</>', $this->path->relativize($to)));
111+
112+
return $copiedFile;
71113
}
72114

73115
private function removeFiles(array $manifest, array $files, string $to)

src/Configurator/EnvConfigurator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111

1212
namespace Symfony\Flex\Configurator;
1313

14+
use Symfony\Flex\Lock;
1415
use Symfony\Flex\Recipe;
1516

1617
/**
1718
* @author Fabien Potencier <[email protected]>
1819
*/
1920
class EnvConfigurator extends AbstractConfigurator
2021
{
21-
public function configure(Recipe $recipe, $vars, array $options = [])
22+
public function configure(Recipe $recipe, $vars, Lock $lock, array $options = [])
2223
{
2324
$this->write('Added environment variable defaults');
2425

@@ -28,7 +29,7 @@ public function configure(Recipe $recipe, $vars, array $options = [])
2829
}
2930
}
3031

31-
public function unconfigure(Recipe $recipe, $vars)
32+
public function unconfigure(Recipe $recipe, $vars, Lock $lock)
3233
{
3334
$this->unconfigureEnvFiles($recipe, $vars);
3435
$this->unconfigurePhpUnit($recipe, $vars);

src/Configurator/GitignoreConfigurator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111

1212
namespace Symfony\Flex\Configurator;
1313

14+
use Symfony\Flex\Lock;
1415
use Symfony\Flex\Recipe;
1516

1617
/**
1718
* @author Fabien Potencier <[email protected]>
1819
*/
1920
class GitignoreConfigurator extends AbstractConfigurator
2021
{
21-
public function configure(Recipe $recipe, $vars, array $options = [])
22+
public function configure(Recipe $recipe, $vars, Lock $lock, array $options = [])
2223
{
2324
$this->write('Added entries to .gitignore');
2425

@@ -39,7 +40,7 @@ public function configure(Recipe $recipe, $vars, array $options = [])
3940
}
4041
}
4142

42-
public function unconfigure(Recipe $recipe, $vars)
43+
public function unconfigure(Recipe $recipe, $vars, Lock $lock)
4344
{
4445
$file = $this->options->get('root-dir').'/.gitignore';
4546
if (!file_exists($file)) {

src/Configurator/MakefileConfigurator.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111

1212
namespace Symfony\Flex\Configurator;
1313

14+
use Symfony\Flex\Lock;
1415
use Symfony\Flex\Recipe;
1516

1617
/**
1718
* @author Fabien Potencier <[email protected]>
1819
*/
1920
class MakefileConfigurator extends AbstractConfigurator
2021
{
21-
public function configure(Recipe $recipe, $definitions, array $options = [])
22+
public function configure(Recipe $recipe, $definitions, Lock $lock, array $options = [])
2223
{
2324
$this->write('Added Makefile entries');
2425

@@ -53,7 +54,7 @@ public function configure(Recipe $recipe, $definitions, array $options = [])
5354
}
5455
}
5556

56-
public function unconfigure(Recipe $recipe, $vars)
57+
public function unconfigure(Recipe $recipe, $vars, Lock $lock)
5758
{
5859
if (!file_exists($makefile = $this->options->get('root-dir').'/Makefile')) {
5960
return;

0 commit comments

Comments
 (0)