Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.64% |
92 / 110 |
|
37.50% |
3 / 8 |
CRAP | |
0.00% |
0 / 1 |
InstalledJson | |
83.64% |
92 / 110 |
|
37.50% |
3 / 8 |
35.21 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
copyInstalledJson | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getJsonFile | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
2.02 | |||
updatePackagePaths | |
73.68% |
14 / 19 |
|
0.00% |
0 / 1 |
5.46 | |||
removeMissingPackages | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 | |||
updateNamespaces | |
78.38% |
29 / 37 |
|
0.00% |
0 / 1 |
17.27 | |||
createAndCleanTargetDirInstalledJson | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
3.00 | |||
cleanupVendorInstalledJson | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Changes "install-path" to point to vendor-prefixed target directory. |
4 | * |
5 | * * create new vendor-prefixed/composer/installed.json file with copied packages |
6 | * * when delete is enabled, update package paths in the original vendor/composer/installed.json |
7 | * * when delete is enabled, remove dead entries in the original vendor/composer/installed.jso |
8 | * |
9 | * @see vendor/composer/installed.json |
10 | * |
11 | * TODO: when delete_vendor_files is used, the original directory still exists so the paths are not updated. |
12 | * |
13 | * @package brianhenryie/strauss |
14 | */ |
15 | |
16 | namespace BrianHenryIE\Strauss\Pipeline\Cleanup; |
17 | |
18 | use BrianHenryIE\Strauss\Composer\ComposerPackage; |
19 | use BrianHenryIE\Strauss\Config\CleanupConfigInterface; |
20 | use BrianHenryIE\Strauss\Helpers\FileSystem; |
21 | use BrianHenryIE\Strauss\Types\DiscoveredSymbols; |
22 | use Composer\Json\JsonFile; |
23 | use Composer\Json\JsonValidationException; |
24 | use Psr\Log\LoggerAwareTrait; |
25 | use Psr\Log\LoggerInterface; |
26 | use Seld\JsonLint\ParsingException; |
27 | |
28 | /** |
29 | * @phpstan-type InstalledJsonPackageSourceArray array{type:string, url:string, reference:string} |
30 | * @phpstan-type InstalledJsonPackageDistArray array{type:string, url:string, reference:string, shasum:string} |
31 | * @phpstan-type InstalledJsonPackageAutoloadArray array<string,array<string,string>> |
32 | * @phpstan-type InstalledJsonPackageAuthorArray array{name:string,email:string} |
33 | * @phpstan-type InstalledJsonPackageSupportArray array{issues:string, source:string} |
34 | * |
35 | * @phpstan-type InstalledJsonPackageArray array{name:string, version:string, version_normalized:string, source:InstalledJsonPackageSourceArray, dist:InstalledJsonPackageDistArray, require:array<string,string>, require-dev:array<string,string>, time:string, type:string, installation-source:string, autoload:InstalledJsonPackageAutoloadArray, notification-url:string, license:array<string>, authors:array<InstalledJsonPackageAuthorArray>, description:string, homepage:string, keywords:array<string>, support:InstalledJsonPackageSupportArray, install-path:string} |
36 | * |
37 | * @phpstan-type InstalledJsonArray array{packages:array<InstalledJsonPackageArray>, dev:bool, dev-package-names:array<string>} |
38 | */ |
39 | class InstalledJson |
40 | { |
41 | use LoggerAwareTrait; |
42 | |
43 | protected CleanupConfigInterface $config; |
44 | |
45 | protected FileSystem $filesystem; |
46 | |
47 | public function __construct( |
48 | CleanupConfigInterface $config, |
49 | FileSystem $filesystem, |
50 | LoggerInterface $logger |
51 | ) { |
52 | $this->config = $config; |
53 | $this->filesystem = $filesystem; |
54 | |
55 | $this->setLogger($logger); |
56 | } |
57 | |
58 | protected function copyInstalledJson(): void |
59 | { |
60 | $this->logger->info('Copying vendor/composer/installed.json to vendor-prefixed/composer/installed.json'); |
61 | |
62 | $this->filesystem->copy( |
63 | $this->config->getVendorDirectory() . 'composer/installed.json', |
64 | $this->config->getTargetDirectory() . 'composer/installed.json' |
65 | ); |
66 | |
67 | $this->logger->debug('Copied vendor/composer/installed.json to vendor-prefixed/composer/installed.json'); |
68 | $this->logger->debug($this->filesystem->read($this->config->getTargetDirectory() . 'composer/installed.json')); |
69 | } |
70 | |
71 | /** |
72 | * @throws JsonValidationException |
73 | * @throws ParsingException |
74 | */ |
75 | protected function getJsonFile(string $vendorDir): JsonFile |
76 | { |
77 | $installedJsonFile = new JsonFile( |
78 | sprintf( |
79 | '%scomposer/installed.json', |
80 | $vendorDir |
81 | ) |
82 | ); |
83 | if (!$installedJsonFile->exists()) { |
84 | $this->logger->error('Expected vendor/composer/installed.json does not exist.'); |
85 | throw new \Exception('Expected vendor/composer/installed.json does not exist.'); |
86 | } |
87 | |
88 | $installedJsonFile->validateSchema(JsonFile::LAX_SCHEMA); |
89 | |
90 | $this->logger->info('Loaded installed.json file: ' . $installedJsonFile->getPath()); |
91 | |
92 | return $installedJsonFile; |
93 | } |
94 | |
95 | /** |
96 | * @param InstalledJsonArray $installedJsonArray |
97 | * @param array<string,ComposerPackage> $flatDependencyTree |
98 | */ |
99 | protected function updatePackagePaths(array $installedJsonArray, array $flatDependencyTree): array |
100 | { |
101 | |
102 | foreach ($installedJsonArray['packages'] as $key => $package) { |
103 | // Skip packages that were never copied in the first place. |
104 | if (!in_array($package['name'], array_keys($flatDependencyTree))) { |
105 | $this->logger->debug('Skipping package: ' . $package['name']); |
106 | continue; |
107 | } |
108 | $this->logger->info('Checking package: ' . $package['name']); |
109 | |
110 | // `composer/` is here because the install-path is relative to the `vendor/composer` directory. |
111 | $packageDir = $this->config->getVendorDirectory() . 'composer/' . $package['install-path'] . '/'; |
112 | if (!$this->filesystem->directoryExists($packageDir)) { |
113 | $this->logger->debug('Original package directory does not exist at : ' . $packageDir); |
114 | |
115 | $newInstallPath = $this->config->getTargetDirectory() . str_replace('../', '', $package['install-path']); |
116 | |
117 | if (!$this->filesystem->directoryExists($newInstallPath)) { |
118 | $this->logger->warning('Target package directory unexpectedly DOES NOT exist: ' . $newInstallPath); |
119 | continue; |
120 | } |
121 | |
122 | $newRelativePath = $this->filesystem->getRelativePath( |
123 | $this->config->getVendorDirectory() . 'composer/', |
124 | $newInstallPath |
125 | ); |
126 | |
127 | $installedJsonArray['packages'][$key]['install-path'] = $newRelativePath; |
128 | } else { |
129 | $this->logger->debug('Original package directory exists at : ' . $packageDir); |
130 | } |
131 | } |
132 | return $installedJsonArray; |
133 | } |
134 | |
135 | |
136 | /** |
137 | * Remove packages from `installed.json` whose target directory does not exist |
138 | * |
139 | * E.g. after the file is copied to the target directory, this will remove dev dependencies and unmodified dependencies from the second installed.json |
140 | */ |
141 | protected function removeMissingPackages(array $installedJsonArray, string $vendorDir): array |
142 | { |
143 | foreach ($installedJsonArray['packages'] as $key => $package) { |
144 | $path = $vendorDir . 'composer/' . $package['install-path']; |
145 | $pathExists = $this->filesystem->directoryExists($path); |
146 | if (!$pathExists) { |
147 | $this->logger->info('Removing package from installed.json: ' . $package['name']); |
148 | unset($installedJsonArray['packages'][$key]); |
149 | } |
150 | } |
151 | return $installedJsonArray; |
152 | } |
153 | |
154 | |
155 | protected function updateNamespaces(array $installedJsonArray, DiscoveredSymbols $discoveredSymbols): array |
156 | { |
157 | $discoveredNamespaces = $discoveredSymbols->getNamespaces(); |
158 | |
159 | foreach ($installedJsonArray['packages'] as $key => $package) { |
160 | if (!isset($package['autoload'])) { |
161 | // woocommerce/action-scheduler |
162 | $this->logger->debug('Package has no autoload key: ' . $package['name'] . ' ' . $package['type']); |
163 | continue; |
164 | } |
165 | |
166 | $autoload_key = $package['autoload']; |
167 | if (!isset($autoload_key['classmap'])) { |
168 | $autoload_key['classmap'] = []; |
169 | } |
170 | foreach ($autoload_key as $type => $autoload) { |
171 | switch ($type) { |
172 | case 'psr-0': |
173 | foreach (array_values((array) $autoload_key['psr-0']) as $relativePath) { |
174 | $packageRelativePath = $package['install-path']; |
175 | if (1 === preg_match('#.*'.preg_quote($this->filesystem->normalize($this->config->getTargetDirectory()), '#').'/(.*)#', $packageRelativePath, $matches)) { |
176 | $packageRelativePath = $matches[1]; |
177 | } |
178 | if ($this->filesystem->directoryExists($this->config->getTargetDirectory() . $packageRelativePath . $relativePath)) { |
179 | $autoload_key['classmap'][] = $relativePath; |
180 | } |
181 | } |
182 | // Intentionally fall through |
183 | // Although the PSR-0 implementation here is a bit of a hack. |
184 | case 'psr-4': |
185 | /** |
186 | * e.g. |
187 | * * {"psr-4":{"Psr\\Log\\":"Psr\/Log\/"}} |
188 | * * {"psr-4":{"":"src\/"}} |
189 | * * {"psr-4":{"Symfony\\Polyfill\\Mbstring\\":""}} |
190 | * * {"psr-0":{"PayPal":"lib\/"}} |
191 | */ |
192 | foreach ($autoload_key[$type] as $originalNamespace => $packageRelativeDirectory) { |
193 | // Replace $originalNamespace with updated namespace |
194 | |
195 | // Just for dev – find a package like this and write a test for it. |
196 | if (empty($originalNamespace)) { |
197 | // In the case of `nesbot/carbon`, it uses an empty namespace but the classes are in the `Carbon` |
198 | // namespace, so using `override_autoload` should be a good solution if this proves to be an issue. |
199 | // The package directory will be updated, so for whatever reason the original empty namespace |
200 | // works, maybe the updated namespace will work too. |
201 | $this->logger->warning('Empty namespace found in autoload. Behaviour is not fully documented: ' . $package['name']); |
202 | continue; |
203 | } |
204 | |
205 | $trimmedOriginalNamespace = trim($originalNamespace, '\\'); |
206 | |
207 | $this->logger->info('Checking '.$type.' namespace: ' . $trimmedOriginalNamespace); |
208 | |
209 | if (isset($discoveredNamespaces[$trimmedOriginalNamespace])) { |
210 | $namespaceSymbol = $discoveredNamespaces[$trimmedOriginalNamespace]; |
211 | } else { |
212 | $this->logger->debug('Namespace not found in list of changes: ' . $trimmedOriginalNamespace); |
213 | continue; |
214 | } |
215 | |
216 | if ($trimmedOriginalNamespace === trim($namespaceSymbol->getReplacement(), '\\')) { |
217 | $this->logger->debug('Namespace is unchanged: ' . $trimmedOriginalNamespace); |
218 | continue; |
219 | } |
220 | |
221 | // Update the namespace if it has changed. |
222 | $this->logger->info('Updating namespace: ' . $trimmedOriginalNamespace . ' => ' . $namespaceSymbol->getReplacement()); |
223 | $autoload_key[$type][str_replace($trimmedOriginalNamespace, $namespaceSymbol->getReplacement(), $originalNamespace)] = $autoload_key[$type][$originalNamespace]; |
224 | unset($autoload_key[$type][$originalNamespace]); |
225 | |
226 | // if (is_array($packageRelativeDirectory)) { |
227 | // $autoload_key[$type][$originalNamespace] = array_filter( |
228 | // $packageRelativeDirectory, |
229 | // function ($dir) use ($packageDir) { |
230 | // $dir = $packageDir . $dir; |
231 | // $exists = $this->filesystem->directoryExists($dir) || $this->filesystem->fileExists($dir); |
232 | // if (!$exists) { |
233 | // $this->logger->info('Removing non-existent directory from autoload: ' . $dir); |
234 | // } else { |
235 | // $this->logger->debug('Keeping directory in autoload: ' . $dir); |
236 | // } |
237 | // return $exists; |
238 | // } |
239 | // ); |
240 | // } else { |
241 | // $dir = $packageDir . $packageRelativeDirectory; |
242 | // if (! ($this->filesystem->directoryExists($dir) || $this->filesystem->fileExists($dir))) { |
243 | // $this->logger->info('Removing non-existent directory from autoload: ' . $dir); |
244 | // // /../../../vendor-prefixed/lib |
245 | // unset($autoload_key[$type][$originalNamespace]); |
246 | // } else { |
247 | // $this->logger->debug('Keeping directory in autoload: ' . $dir); |
248 | // } |
249 | // } |
250 | } |
251 | break; |
252 | default: // files, classmap, psr-0 |
253 | /** |
254 | * E.g. |
255 | * |
256 | * * {"classmap":["src\/"]} |
257 | * * {"files":["src\/functions.php"]} |
258 | * |
259 | * Also: |
260 | * * {"exclude-from-classmap":["\/Tests\/"]} |
261 | */ |
262 | |
263 | // $autoload_key[$type] = array_filter($autoload, function ($file) use ($packageDir) { |
264 | // $filename = $packageDir . '/' . $file; |
265 | // $exists = $this->filesystem->directoryExists($filename) || $this->filesystem->fileExists($filename); |
266 | // if (!$exists) { |
267 | // $this->logger->info('Removing non-existent file from autoload: ' . $filename); |
268 | // } else { |
269 | // $this->logger->debug('Keeping file in autoload: ' . $filename); |
270 | // } |
271 | // }); |
272 | break; |
273 | } |
274 | } |
275 | $installedJsonArray['packages'][$key]['autoload'] = array_filter($autoload_key); |
276 | } |
277 | |
278 | return $installedJsonArray; |
279 | } |
280 | /** |
281 | * @param array<string,ComposerPackage> $flatDependencyTree |
282 | * @param DiscoveredSymbols $discoveredSymbols |
283 | */ |
284 | public function createAndCleanTargetDirInstalledJson(array $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void |
285 | { |
286 | $this->copyInstalledJson(); |
287 | |
288 | $targetDir = $this->config->getTargetDirectory(); |
289 | |
290 | $installedJsonFile = $this->getJsonFile($targetDir); |
291 | |
292 | /** |
293 | * @var InstalledJsonArray $installedJsonArray |
294 | */ |
295 | $installedJsonArray = $installedJsonFile->read(); |
296 | |
297 | $this->logger->debug('Installed.json before: ' . json_encode($installedJsonArray)); |
298 | |
299 | $installedJsonArray = $this->updatePackagePaths($installedJsonArray, $flatDependencyTree); |
300 | |
301 | $installedJsonArray = $this->removeMissingPackages($installedJsonArray, $targetDir); |
302 | |
303 | $installedJsonArray = $this->updateNamespaces($installedJsonArray, $discoveredSymbols); |
304 | |
305 | foreach ($installedJsonArray['packages'] as $index => $package) { |
306 | if (!in_array($package['name'], array_keys($flatDependencyTree))) { |
307 | unset($installedJsonArray['packages'][$index]); |
308 | } |
309 | } |
310 | $installedJsonArray['dev'] = false; |
311 | $installedJsonArray['dev-package-names'] = []; |
312 | |
313 | $this->logger->debug('Installed.json after: ' . json_encode($installedJsonArray)); |
314 | |
315 | $this->logger->info('Writing installed.json to ' . $targetDir); |
316 | |
317 | $installedJsonFile->write($installedJsonArray); |
318 | |
319 | $this->logger->info('Installed.json written to ' . $targetDir); |
320 | } |
321 | |
322 | |
323 | /** |
324 | * Composer creates a file `vendor/composer/installed.json` which is uses when running `composer dump-autoload`. |
325 | * When `delete-vendor-packages` or `delete-vendor-files` is true, files and directories which have been deleted |
326 | * must also be removed from `installed.json` or Composer will throw an error. |
327 | * |
328 | * @param array<string,ComposerPackage> $flatDependencyTree |
329 | */ |
330 | public function cleanupVendorInstalledJson(array $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void |
331 | { |
332 | $this->logger->info('Cleaning up installed.json'); |
333 | |
334 | $vendorDir = $this->config->getVendorDirectory(); |
335 | |
336 | $vendorInstalledJsonFile = $this->getJsonFile($vendorDir); |
337 | |
338 | /** |
339 | * @var InstalledJsonArray $installedJsonArray |
340 | */ |
341 | $installedJsonArray = $vendorInstalledJsonFile->read(); |
342 | |
343 | $installedJsonArray = $this->updatePackagePaths($installedJsonArray, $flatDependencyTree); |
344 | |
345 | $installedJsonArray = $this->updateNamespaces($installedJsonArray, $discoveredSymbols); |
346 | |
347 | $installedJsonArray = $this->removeMissingPackages($installedJsonArray, $vendorDir); |
348 | |
349 | $vendorInstalledJsonFile->write($installedJsonArray); |
350 | } |
351 | } |