Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 120 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
Cleanup | |
0.00% |
0 / 120 |
|
0.00% |
0 / 8 |
1406 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
deleteFiles | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
cleanupVendorInstalledJson | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
rebuildVendorAutoloader | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
6 | |||
deleteEmptyDirectories | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
72 | |||
dirIsEmpty | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doIsDeleteVendorPackages | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
doIsDeleteVendorFiles | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * Deletes source files and empty directories. |
4 | */ |
5 | |
6 | namespace BrianHenryIE\Strauss\Pipeline\Cleanup; |
7 | |
8 | use BrianHenryIE\Strauss\Composer\ComposerPackage; |
9 | use BrianHenryIE\Strauss\Config\CleanupConfigInterface; |
10 | use BrianHenryIE\Strauss\Files\File; |
11 | use BrianHenryIE\Strauss\Files\FileWithDependency; |
12 | use BrianHenryIE\Strauss\Helpers\FileSystem; |
13 | use BrianHenryIE\Strauss\Pipeline\Autoload\DumpAutoload; |
14 | use BrianHenryIE\Strauss\Types\DiscoveredSymbols; |
15 | use Composer\Autoload\AutoloadGenerator; |
16 | use Composer\Config as ComposerConfig; |
17 | use Composer\Factory; |
18 | use Composer\IO\NullIO; |
19 | use Composer\Json\JsonFile; |
20 | use Composer\Repository\InstalledFilesystemRepository; |
21 | use League\Flysystem\FilesystemException; |
22 | use Psr\Log\LoggerAwareTrait; |
23 | use Psr\Log\LoggerInterface; |
24 | |
25 | class Cleanup |
26 | { |
27 | use LoggerAwareTrait; |
28 | |
29 | protected Filesystem $filesystem; |
30 | |
31 | protected bool $isDeleteVendorFiles; |
32 | protected bool $isDeleteVendorPackages; |
33 | |
34 | protected CleanupConfigInterface $config; |
35 | |
36 | public function __construct( |
37 | CleanupConfigInterface $config, |
38 | Filesystem $filesystem, |
39 | LoggerInterface $logger |
40 | ) { |
41 | $this->config = $config; |
42 | $this->logger = $logger; |
43 | |
44 | $this->isDeleteVendorFiles = $config->isDeleteVendorFiles() && $config->getTargetDirectory() !== $config->getVendorDirectory(); |
45 | $this->isDeleteVendorPackages = $config->isDeleteVendorPackages() && $config->getTargetDirectory() !== $config->getVendorDirectory(); |
46 | |
47 | $this->filesystem = $filesystem; |
48 | } |
49 | |
50 | /** |
51 | * Maybe delete the source files that were copied (depending on config), |
52 | * then delete empty directories. |
53 | * |
54 | * @param File[] $files |
55 | * |
56 | * @throws FilesystemException |
57 | */ |
58 | public function deleteFiles(array $files): void |
59 | { |
60 | if (!$this->isDeleteVendorPackages && !$this->isDeleteVendorFiles) { |
61 | $this->logger->info('No cleanup required.'); |
62 | return; |
63 | } |
64 | |
65 | $this->logger->info('Beginning cleanup.'); |
66 | |
67 | if ($this->isDeleteVendorPackages) { |
68 | $this->doIsDeleteVendorPackages($files); |
69 | } elseif ($this->isDeleteVendorFiles) { |
70 | $this->doIsDeleteVendorFiles($files); |
71 | } |
72 | |
73 | $this->deleteEmptyDirectories($files); |
74 | } |
75 | |
76 | /** @param array<string,ComposerPackage> $flatDependencyTree */ |
77 | public function cleanupVendorInstalledJson(array $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void |
78 | { |
79 | $installedJson = new InstalledJson( |
80 | $this->config, |
81 | $this->filesystem, |
82 | $this->logger |
83 | ); |
84 | |
85 | if ($this->config->getTargetDirectory() !== $this->config->getVendorDirectory() |
86 | && !$this->config->isDeleteVendorFiles() && !$this->config->isDeleteVendorPackages() |
87 | ) { |
88 | $installedJson->cleanTargetDirInstalledJson($flatDependencyTree, $discoveredSymbols); |
89 | } elseif ($this->config->getTargetDirectory() !== $this->config->getVendorDirectory() |
90 | && |
91 | ($this->config->isDeleteVendorFiles() ||$this->config->isDeleteVendorPackages()) |
92 | ) { |
93 | $installedJson->cleanTargetDirInstalledJson($flatDependencyTree, $discoveredSymbols); |
94 | $installedJson->cleanupVendorInstalledJson($flatDependencyTree, $discoveredSymbols); |
95 | } elseif ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) { |
96 | $installedJson->cleanupVendorInstalledJson($flatDependencyTree, $discoveredSymbols); |
97 | } |
98 | } |
99 | |
100 | /** |
101 | * After packages or files have been deleted, the autoloader still contains references to them, in particular |
102 | * `files` are `require`d on boot (whereas classes are on demand) so that must be fixed. |
103 | * |
104 | * Assumes {@see Cleanup::cleanupVendorInstalledJson()} has been called first. |
105 | * |
106 | * TODO refactor so this object is passed around rather than reloaded. |
107 | * |
108 | * Shares a lot of code with {@see DumpAutoload::generatedPrefixedAutoloader()} but I've done lots of work |
109 | * on that in another branch so I don't want to cause merge conflicts. |
110 | */ |
111 | public function rebuildVendorAutoloader(): void |
112 | { |
113 | if ($this->config->isDryRun()) { |
114 | return; |
115 | } |
116 | |
117 | $projectComposerJson = new JsonFile($this->config->getProjectDirectory() . 'composer.json'); |
118 | $projectComposerJsonArray = $projectComposerJson->read(); |
119 | $composer = Factory::create(new NullIO(), $projectComposerJsonArray); |
120 | $installationManager = $composer->getInstallationManager(); |
121 | $package = $composer->getPackage(); |
122 | $config = $composer->getConfig(); |
123 | $generator = new AutoloadGenerator($composer->getEventDispatcher()); |
124 | |
125 | $generator->setClassMapAuthoritative(true); |
126 | $generator->setRunScripts(false); |
127 | // $generator->setApcu($apcu, $apcuPrefix); |
128 | // $generator->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input)); |
129 | $optimize = true; // $input->getOption('optimize') || $config->get('optimize-autoloader'); |
130 | $installedJson = new JsonFile($this->config->getVendorDirectory() . 'composer/installed.json'); |
131 | $localRepo = new InstalledFilesystemRepository($installedJson); |
132 | $strictAmbiguous = false; // $input->getOption('strict-ambiguous') |
133 | $installedJsonArray = $installedJson->read(); |
134 | $generator->setDevMode($installedJsonArray['dev'] ?? false); |
135 | // This will output the autoload_static.php etc. files to `vendor/composer`. |
136 | $generator->dump( |
137 | $config, |
138 | $localRepo, |
139 | $package, |
140 | $installationManager, |
141 | 'composer', |
142 | $optimize, |
143 | null, |
144 | $composer->getLocker(), |
145 | $strictAmbiguous |
146 | ); |
147 | } |
148 | |
149 | /** |
150 | * @throws FilesystemException |
151 | */ |
152 | protected function deleteEmptyDirectories(array $files) |
153 | { |
154 | $this->logger->info('Deleting empty directories.'); |
155 | |
156 | $sourceFiles = array_map( |
157 | fn($file) => $file->getSourcePath(), |
158 | $files |
159 | ); |
160 | |
161 | // Get the root folders of the moved files. |
162 | $rootSourceDirectories = []; |
163 | foreach ($sourceFiles as $sourceFile) { |
164 | $arr = explode("/", $sourceFile, 2); |
165 | $dir = $arr[0]; |
166 | $rootSourceDirectories[ $dir ] = $dir; |
167 | } |
168 | $rootSourceDirectories = array_map( |
169 | function (string $path): string { |
170 | return $this->config->getVendorDirectory() . $path; |
171 | }, |
172 | array_keys($rootSourceDirectories) |
173 | ); |
174 | |
175 | foreach ($rootSourceDirectories as $rootSourceDirectory) { |
176 | if (!$this->filesystem->directoryExists($rootSourceDirectory) || is_link($rootSourceDirectory)) { |
177 | continue; |
178 | } |
179 | |
180 | $dirList = $this->filesystem->listContents($rootSourceDirectory, true); |
181 | |
182 | $allFilePaths = array_map( |
183 | fn($file) => $file->path(), |
184 | $dirList->toArray() |
185 | ); |
186 | |
187 | // Sort by longest path first, so subdirectories are deleted before the parent directories are checked. |
188 | usort( |
189 | $allFilePaths, |
190 | fn($a, $b) => count(explode('/', $b)) - count(explode('/', $a)) |
191 | ); |
192 | |
193 | foreach ($allFilePaths as $filePath) { |
194 | if ($this->filesystem->directoryExists($filePath) |
195 | && $this->dirIsEmpty($filePath) |
196 | ) { |
197 | $this->logger->debug('Deleting empty directory ' . $filePath); |
198 | $this->filesystem->deleteDirectory($filePath); |
199 | } |
200 | } |
201 | } |
202 | |
203 | // foreach ($this->filesystem->listContents($this->getAbsoluteVendorDir()) as $dirEntry) { |
204 | // if ($dirEntry->isDir() && $this->dirIsEmpty($dirEntry->path()) && !is_link($dirEntry->path())) { |
205 | // $this->logger->info('Deleting empty directory ' . $dirEntry->path()); |
206 | // $this->filesystem->deleteDirectory($dirEntry->path()); |
207 | // } else { |
208 | // $this->logger->debug('Skipping non-empty directory ' . $dirEntry->path()); |
209 | // } |
210 | // } |
211 | } |
212 | |
213 | // TODO: Move to FileSystem class. |
214 | protected function dirIsEmpty(string $dir): bool |
215 | { |
216 | // TODO BUG this deletes directories with only symlinks inside. How does it behave with hidden files? |
217 | return empty($this->filesystem->listContents($dir)->toArray()); |
218 | } |
219 | |
220 | /** |
221 | * @param array<File> $files |
222 | */ |
223 | protected function doIsDeleteVendorPackages(array $files) |
224 | { |
225 | $this->logger->info('Deleting original vendor packages.'); |
226 | |
227 | $packages = []; |
228 | foreach ($files as $file) { |
229 | if ($file instanceof FileWithDependency) { |
230 | $packages[ $file->getDependency()->getPackageName() ] = $file->getDependency(); |
231 | } |
232 | } |
233 | |
234 | /** @var ComposerPackage $package */ |
235 | foreach ($packages as $package) { |
236 | // Normal package. |
237 | if ($this->filesystem->isSubDirOf($this->config->getVendorDirectory(), $package->getPackageAbsolutePath())) { |
238 | $this->logger->info('Deleting ' . $package->getPackageAbsolutePath()); |
239 | |
240 | $this->filesystem->deleteDirectory($package->getPackageAbsolutePath()); |
241 | } else { |
242 | // TODO: log _where_ the symlink is pointing to. |
243 | $this->logger->info('Deleting symlink at ' . $package->getRelativePath()); |
244 | |
245 | // If it's a symlink, remove the symlink in the directory |
246 | $symlinkPath = |
247 | rtrim( |
248 | $this->config->getVendorDirectory() . $package->getRelativePath(), |
249 | '/' |
250 | ); |
251 | |
252 | if (false !== strpos('WIN', PHP_OS)) { |
253 | /** |
254 | * `unlink()` will not work on Windows. `rmdir()` will not work if there are files in the directory. |
255 | * "On windows, take care that `is_link()` returns false for Junctions." |
256 | * |
257 | * @see https://www.php.net/manual/en/function.is-link.php#113263 |
258 | * @see https://stackoverflow.com/a/18262809/336146 |
259 | */ |
260 | rmdir($symlinkPath); |
261 | } else { |
262 | unlink($symlinkPath); |
263 | } |
264 | } |
265 | if ($this->dirIsEmpty(dirname($package->getPackageAbsolutePath()))) { |
266 | $this->logger->info('Deleting empty directory ' . dirname($package->getPackageAbsolutePath())); |
267 | $this->filesystem->deleteDirectory(dirname($package->getPackageAbsolutePath())); |
268 | } |
269 | } |
270 | } |
271 | |
272 | /** |
273 | * @param array $files |
274 | * |
275 | * @throws FilesystemException |
276 | */ |
277 | public function doIsDeleteVendorFiles(array $files) |
278 | { |
279 | $this->logger->info('Deleting original vendor files.'); |
280 | |
281 | foreach ($files as $file) { |
282 | if (! $file->isDoDelete()) { |
283 | $this->logger->debug('Skipping/preserving ' . $file->getSourcePath()); |
284 | continue; |
285 | } |
286 | |
287 | $sourceRelativePath = $file->getSourcePath(); |
288 | |
289 | $this->logger->info('Deleting ' . $sourceRelativePath); |
290 | |
291 | $this->filesystem->delete($file->getSourcePath()); |
292 | |
293 | $file->setDidDelete(true); |
294 | } |
295 | } |
296 | } |