|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/** |
|
4
|
|
|
* @file |
|
5
|
|
|
* Provides a way to patch Composer packages after installation. |
|
6
|
|
|
*/ |
|
7
|
|
|
|
|
8
|
|
|
namespace cweagans\Composer; |
|
9
|
|
|
|
|
10
|
|
|
use Composer\Composer; |
|
11
|
|
|
use Composer\DependencyResolver\Operation\InstallOperation; |
|
12
|
|
|
use Composer\DependencyResolver\Operation\UninstallOperation; |
|
13
|
|
|
use Composer\DependencyResolver\Operation\UpdateOperation; |
|
14
|
|
|
use Composer\DependencyResolver\Operation\OperationInterface; |
|
15
|
|
|
use Composer\EventDispatcher\EventSubscriberInterface; |
|
16
|
|
|
use Composer\IO\IOInterface; |
|
17
|
|
|
use Composer\Package\AliasPackage; |
|
18
|
|
|
use Composer\Package\PackageInterface; |
|
19
|
|
|
use Composer\Plugin\PluginInterface; |
|
20
|
|
|
use Composer\Installer\PackageEvents; |
|
21
|
|
|
use Composer\Script\Event; |
|
22
|
|
|
use Composer\Script\ScriptEvents; |
|
23
|
|
|
use Composer\Installer\PackageEvent; |
|
24
|
|
|
use Composer\Util\ProcessExecutor; |
|
25
|
|
|
use Composer\Util\RemoteFilesystem; |
|
26
|
|
|
use Symfony\Component\Process\Process; |
|
27
|
|
|
|
|
28
|
|
|
class Patches implements PluginInterface, EventSubscriberInterface { |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* @var Composer $composer |
|
32
|
|
|
*/ |
|
33
|
|
|
protected $composer; |
|
34
|
|
|
/** |
|
35
|
|
|
* @var IOInterface $io |
|
36
|
|
|
*/ |
|
37
|
|
|
protected $io; |
|
38
|
|
|
/** |
|
39
|
|
|
* @var EventDispatcher $eventDispatcher |
|
40
|
|
|
*/ |
|
41
|
|
|
protected $eventDispatcher; |
|
42
|
|
|
/** |
|
43
|
|
|
* @var ProcessExecutor $executor |
|
44
|
|
|
*/ |
|
45
|
|
|
protected $executor; |
|
46
|
|
|
/** |
|
47
|
|
|
* @var array $patches |
|
48
|
|
|
*/ |
|
49
|
|
|
protected $patches; |
|
50
|
|
|
|
|
51
|
|
|
/** |
|
52
|
|
|
* Apply plugin modifications to composer |
|
53
|
|
|
* |
|
54
|
|
|
* @param Composer $composer |
|
55
|
|
|
* @param IOInterface $io |
|
56
|
|
|
*/ |
|
57
|
|
|
public function activate(Composer $composer, IOInterface $io) { |
|
58
|
|
|
$this->composer = $composer; |
|
59
|
|
|
$this->io = $io; |
|
60
|
|
|
$this->eventDispatcher = $composer->getEventDispatcher(); |
|
61
|
|
|
$this->executor = new ProcessExecutor($this->io); |
|
62
|
|
|
$this->patches = array(); |
|
63
|
|
|
$this->installedPatches = array(); |
|
64
|
|
|
} |
|
65
|
|
|
|
|
66
|
|
|
/** |
|
67
|
|
|
* Returns an array of event names this subscriber wants to listen to. |
|
68
|
|
|
*/ |
|
69
|
|
|
public static function getSubscribedEvents() { |
|
70
|
|
|
return array( |
|
71
|
|
|
ScriptEvents::PRE_INSTALL_CMD => array('checkPatches'), |
|
72
|
|
|
ScriptEvents::PRE_UPDATE_CMD => array('checkPatches'), |
|
73
|
|
|
PackageEvents::PRE_PACKAGE_INSTALL => array('gatherPatches'), |
|
74
|
|
|
PackageEvents::PRE_PACKAGE_UPDATE => array('gatherPatches'), |
|
75
|
|
|
// The following is a higher weight for compatibility with |
|
76
|
|
|
// https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with |
|
77
|
|
|
// every Composer plugin which deploys downloaded packages to other locations. |
|
78
|
|
|
// In such cases you want that those plugins deploy patched files so they have to run after |
|
79
|
|
|
// the "composer-patches" plugin. |
|
80
|
|
|
// @see: https://github.com/cweagans/composer-patches/pull/153 |
|
81
|
|
|
PackageEvents::POST_PACKAGE_INSTALL => array('postInstall', 10), |
|
82
|
|
|
PackageEvents::POST_PACKAGE_UPDATE => array('postInstall', 10), |
|
83
|
|
|
); |
|
84
|
|
|
} |
|
85
|
|
|
|
|
86
|
|
|
/** |
|
87
|
|
|
* Before running composer install, |
|
88
|
|
|
* @param Event $event |
|
89
|
|
|
*/ |
|
90
|
|
|
public function checkPatches(Event $event) { |
|
91
|
|
|
if (!$this->isPatchingEnabled()) { |
|
92
|
|
|
return; |
|
93
|
|
|
} |
|
94
|
|
|
|
|
95
|
|
|
try { |
|
96
|
|
|
$repositoryManager = $this->composer->getRepositoryManager(); |
|
97
|
|
|
$localRepository = $repositoryManager->getLocalRepository(); |
|
98
|
|
|
$installationManager = $this->composer->getInstallationManager(); |
|
99
|
|
|
$packages = $localRepository->getPackages(); |
|
100
|
|
|
|
|
101
|
|
|
$extra = $this->composer->getPackage()->getExtra(); |
|
102
|
|
|
$patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array(); |
|
103
|
|
|
|
|
104
|
|
|
$tmp_patches = $this->grabPatches(); |
|
105
|
|
|
foreach ($packages as $package) { |
|
106
|
|
|
$extra = $package->getExtra(); |
|
107
|
|
View Code Duplication |
if (isset($extra['patches'])) { |
|
|
|
|
|
|
108
|
|
|
if (isset($patches_ignore[$package->getName()])) { |
|
109
|
|
|
foreach ($patches_ignore[$package->getName()] as $package_name => $patches) { |
|
110
|
|
|
if (isset($extra['patches'][$package_name])) { |
|
111
|
|
|
$extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches); |
|
112
|
|
|
} |
|
113
|
|
|
} |
|
114
|
|
|
} |
|
115
|
|
|
$this->installedPatches[$package->getName()] = $extra['patches']; |
|
116
|
|
|
} |
|
117
|
|
|
$patches = isset($extra['patches']) ? $extra['patches'] : array(); |
|
118
|
|
|
$tmp_patches = $this->arrayMergeRecursiveDistinct($tmp_patches, $patches); |
|
119
|
|
|
} |
|
120
|
|
|
|
|
121
|
|
|
if ($tmp_patches == FALSE) { |
|
122
|
|
|
$this->io->write('<info>No patches supplied.</info>'); |
|
123
|
|
|
return; |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
// Remove packages for which the patch set has changed. |
|
127
|
|
|
foreach ($packages as $package) { |
|
128
|
|
|
if (!($package instanceof AliasPackage)) { |
|
|
|
|
|
|
129
|
|
|
$package_name = $package->getName(); |
|
130
|
|
|
$extra = $package->getExtra(); |
|
131
|
|
|
$has_patches = isset($tmp_patches[$package_name]); |
|
132
|
|
|
$has_applied_patches = isset($extra['patches_applied']) && count($extra['patches_applied']) > 0; |
|
133
|
|
|
if (($has_patches && !$has_applied_patches) |
|
134
|
|
|
|| (!$has_patches && $has_applied_patches) |
|
135
|
|
|
|| ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra['patches_applied'])) { |
|
136
|
|
|
$uninstallOperation = new UninstallOperation($package, 'Removing package so it can be re-installed and re-patched.'); |
|
137
|
|
|
$this->io->write('<info>Removing package ' . $package_name . ' so that it can be re-installed and re-patched.</info>'); |
|
138
|
|
|
$installationManager->uninstall($localRepository, $uninstallOperation); |
|
139
|
|
|
} |
|
140
|
|
|
} |
|
141
|
|
|
} |
|
142
|
|
|
} |
|
143
|
|
|
// If the Locker isn't available, then we don't need to do this. |
|
144
|
|
|
// It's the first time packages have been installed. |
|
145
|
|
|
catch (\LogicException $e) { |
|
146
|
|
|
return; |
|
147
|
|
|
} |
|
148
|
|
|
} |
|
149
|
|
|
|
|
150
|
|
|
/** |
|
151
|
|
|
* Gather patches from dependencies and store them for later use. |
|
152
|
|
|
* |
|
153
|
|
|
* @param PackageEvent $event |
|
154
|
|
|
*/ |
|
155
|
|
|
public function gatherPatches(PackageEvent $event) { |
|
156
|
|
|
// If we've already done this, then don't do it again. |
|
157
|
|
|
if (isset($this->patches['_patchesGathered'])) { |
|
158
|
|
|
$this->io->write('<info>Patches already gathered. Skipping</info>', TRUE, IOInterface::VERBOSE); |
|
159
|
|
|
return; |
|
160
|
|
|
} |
|
161
|
|
|
// If patching has been disabled, bail out here. |
|
162
|
|
|
elseif (!$this->isPatchingEnabled()) { |
|
163
|
|
|
$this->io->write('<info>Patching is disabled. Skipping.</info>', TRUE, IOInterface::VERBOSE); |
|
164
|
|
|
return; |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
$this->patches = $this->grabPatches(); |
|
|
|
|
|
|
168
|
|
|
if (empty($this->patches)) { |
|
169
|
|
|
$this->io->write('<info>No patches supplied.</info>'); |
|
170
|
|
|
} |
|
171
|
|
|
|
|
172
|
|
|
$extra = $this->composer->getPackage()->getExtra(); |
|
173
|
|
|
$patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array(); |
|
174
|
|
|
|
|
175
|
|
|
// Now add all the patches from dependencies that will be installed. |
|
176
|
|
|
$operations = $event->getOperations(); |
|
177
|
|
|
$this->io->write('<info>Gathering patches for dependencies. This might take a minute.</info>'); |
|
178
|
|
|
foreach ($operations as $operation) { |
|
179
|
|
|
if ($operation->getJobType() == 'install' || $operation->getJobType() == 'update') { |
|
180
|
|
|
$package = $this->getPackageFromOperation($operation); |
|
181
|
|
|
$extra = $package->getExtra(); |
|
182
|
|
View Code Duplication |
if (isset($extra['patches'])) { |
|
|
|
|
|
|
183
|
|
|
if (isset($patches_ignore[$package->getName()])) { |
|
184
|
|
|
foreach ($patches_ignore[$package->getName()] as $package_name => $patches) { |
|
185
|
|
|
if (isset($extra['patches'][$package_name])) { |
|
186
|
|
|
$extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches); |
|
187
|
|
|
} |
|
188
|
|
|
} |
|
189
|
|
|
} |
|
190
|
|
|
$this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $extra['patches']); |
|
191
|
|
|
} |
|
192
|
|
|
// Unset installed patches for this package |
|
193
|
|
|
if(isset($this->installedPatches[$package->getName()])) { |
|
194
|
|
|
unset($this->installedPatches[$package->getName()]); |
|
195
|
|
|
} |
|
196
|
|
|
} |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
// Merge installed patches from dependencies that did not receive an update. |
|
200
|
|
|
foreach ($this->installedPatches as $patches) { |
|
201
|
|
|
$this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $patches); |
|
202
|
|
|
} |
|
203
|
|
|
|
|
204
|
|
|
// If we're in verbose mode, list the projects we're going to patch. |
|
205
|
|
|
if ($this->io->isVerbose()) { |
|
206
|
|
|
foreach ($this->patches as $package => $patches) { |
|
207
|
|
|
$number = count($patches); |
|
208
|
|
|
$this->io->write('<info>Found ' . $number . ' patches for ' . $package . '.</info>'); |
|
209
|
|
|
} |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
// Make sure we don't gather patches again. Extra keys in $this->patches |
|
213
|
|
|
// won't hurt anything, so we'll just stash it there. |
|
214
|
|
|
$this->patches['_patchesGathered'] = TRUE; |
|
215
|
|
|
} |
|
216
|
|
|
|
|
217
|
|
|
/** |
|
218
|
|
|
* Get the patches from root composer or external file |
|
219
|
|
|
* @return Patches |
|
220
|
|
|
* @throws \Exception |
|
221
|
|
|
*/ |
|
222
|
|
|
public function grabPatches() { |
|
223
|
|
|
// First, try to get the patches from the root composer.json. |
|
224
|
|
|
$extra = $this->composer->getPackage()->getExtra(); |
|
225
|
|
|
if (isset($extra['patches'])) { |
|
226
|
|
|
$this->io->write('<info>Gathering patches for root package.</info>'); |
|
227
|
|
|
$patches = $extra['patches']; |
|
228
|
|
|
return $patches; |
|
229
|
|
|
} |
|
230
|
|
|
// If it's not specified there, look for a patches-file definition. |
|
231
|
|
|
elseif (isset($extra['patches-file'])) { |
|
232
|
|
|
$this->io->write('<info>Gathering patches from patch file.</info>'); |
|
233
|
|
|
$patches = file_get_contents($extra['patches-file']); |
|
234
|
|
|
$patches = json_decode($patches, TRUE); |
|
235
|
|
|
$error = json_last_error(); |
|
236
|
|
|
if ($error != 0) { |
|
237
|
|
|
switch ($error) { |
|
238
|
|
|
case JSON_ERROR_DEPTH: |
|
239
|
|
|
$msg = ' - Maximum stack depth exceeded'; |
|
240
|
|
|
break; |
|
241
|
|
|
case JSON_ERROR_STATE_MISMATCH: |
|
242
|
|
|
$msg = ' - Underflow or the modes mismatch'; |
|
243
|
|
|
break; |
|
244
|
|
|
case JSON_ERROR_CTRL_CHAR: |
|
245
|
|
|
$msg = ' - Unexpected control character found'; |
|
246
|
|
|
break; |
|
247
|
|
|
case JSON_ERROR_SYNTAX: |
|
248
|
|
|
$msg = ' - Syntax error, malformed JSON'; |
|
249
|
|
|
break; |
|
250
|
|
|
case JSON_ERROR_UTF8: |
|
251
|
|
|
$msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded'; |
|
252
|
|
|
break; |
|
253
|
|
|
default: |
|
254
|
|
|
$msg = ' - Unknown error'; |
|
255
|
|
|
break; |
|
256
|
|
|
} |
|
257
|
|
|
throw new \Exception('There was an error in the supplied patches file:' . $msg); |
|
258
|
|
|
} |
|
259
|
|
|
if (isset($patches['patches'])) { |
|
260
|
|
|
$patches = $patches['patches']; |
|
261
|
|
|
return $patches; |
|
262
|
|
|
} |
|
263
|
|
|
elseif(!$patches) { |
|
264
|
|
|
throw new \Exception('There was an error in the supplied patch file'); |
|
265
|
|
|
} |
|
266
|
|
|
} |
|
267
|
|
|
else { |
|
268
|
|
|
return array(); |
|
|
|
|
|
|
269
|
|
|
} |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
/** |
|
273
|
|
|
* @param PackageEvent $event |
|
274
|
|
|
* @throws \Exception |
|
275
|
|
|
*/ |
|
276
|
|
|
public function postInstall(PackageEvent $event) { |
|
277
|
|
|
|
|
278
|
|
|
// Check if we should exit in failure. |
|
279
|
|
|
$extra = $this->composer->getPackage()->getExtra(); |
|
280
|
|
|
$exitOnFailure = getenv('COMPOSER_EXIT_ON_PATCH_FAILURE') || !empty($extra['composer-exit-on-patch-failure']); |
|
281
|
|
|
|
|
282
|
|
|
// Get the package object for the current operation. |
|
283
|
|
|
$operation = $event->getOperation(); |
|
284
|
|
|
/** @var PackageInterface $package */ |
|
285
|
|
|
$package = $this->getPackageFromOperation($operation); |
|
286
|
|
|
$package_name = $package->getName(); |
|
287
|
|
|
|
|
288
|
|
|
if (!isset($this->patches[$package_name])) { |
|
289
|
|
|
if ($this->io->isVerbose()) { |
|
290
|
|
|
$this->io->write('<info>No patches found for ' . $package_name . '.</info>'); |
|
291
|
|
|
} |
|
292
|
|
|
return; |
|
293
|
|
|
} |
|
294
|
|
|
$this->io->write(' - Applying patches for <info>' . $package_name . '</info>'); |
|
295
|
|
|
|
|
296
|
|
|
// Get the install path from the package object. |
|
297
|
|
|
$manager = $event->getComposer()->getInstallationManager(); |
|
298
|
|
|
$install_path = $manager->getInstaller($package->getType())->getInstallPath($package); |
|
299
|
|
|
|
|
300
|
|
|
// Set up a downloader. |
|
301
|
|
|
$downloader = new RemoteFilesystem($this->io, $this->composer->getConfig()); |
|
302
|
|
|
|
|
303
|
|
|
// Track applied patches in the package info in installed.json |
|
304
|
|
|
$localRepository = $this->composer->getRepositoryManager()->getLocalRepository(); |
|
305
|
|
|
$localPackage = $localRepository->findPackage($package_name, $package->getVersion()); |
|
306
|
|
|
$extra = $localPackage->getExtra(); |
|
307
|
|
|
$extra['patches_applied'] = array(); |
|
308
|
|
|
|
|
309
|
|
|
foreach ($this->patches[$package_name] as $description => $url) { |
|
310
|
|
|
$this->io->write(' <info>' . $url . '</info> (<comment>' . $description. '</comment>)'); |
|
311
|
|
|
try { |
|
312
|
|
|
$this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description)); |
|
313
|
|
|
$this->getAndApplyPatch($downloader, $install_path, $url, $package); |
|
314
|
|
|
$this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description)); |
|
315
|
|
|
$extra['patches_applied'][$description] = $url; |
|
316
|
|
|
} |
|
317
|
|
|
catch (\Exception $e) { |
|
318
|
|
|
$this->io->write(' <error>Could not apply patch! Skipping. The error was: ' . $e->getMessage() . '</error>'); |
|
319
|
|
|
if ($exitOnFailure) { |
|
320
|
|
|
throw new \Exception("Cannot apply patch $description ($url)!"); |
|
321
|
|
|
} |
|
322
|
|
|
} |
|
323
|
|
|
} |
|
324
|
|
|
$localPackage->setExtra($extra); |
|
325
|
|
|
|
|
326
|
|
|
$this->io->write(''); |
|
327
|
|
|
$this->writePatchReport($this->patches[$package_name], $install_path); |
|
328
|
|
|
} |
|
329
|
|
|
|
|
330
|
|
|
/** |
|
331
|
|
|
* Get a Package object from an OperationInterface object. |
|
332
|
|
|
* |
|
333
|
|
|
* @param OperationInterface $operation |
|
334
|
|
|
* @return PackageInterface |
|
335
|
|
|
* @throws \Exception |
|
336
|
|
|
*/ |
|
337
|
|
|
protected function getPackageFromOperation(OperationInterface $operation) { |
|
338
|
|
|
if ($operation instanceof InstallOperation) { |
|
|
|
|
|
|
339
|
|
|
$package = $operation->getPackage(); |
|
340
|
|
|
} |
|
341
|
|
|
elseif ($operation instanceof UpdateOperation) { |
|
|
|
|
|
|
342
|
|
|
$package = $operation->getTargetPackage(); |
|
343
|
|
|
} |
|
344
|
|
|
else { |
|
345
|
|
|
throw new \Exception('Unknown operation: ' . get_class($operation)); |
|
346
|
|
|
} |
|
347
|
|
|
|
|
348
|
|
|
return $package; |
|
349
|
|
|
} |
|
350
|
|
|
|
|
351
|
|
|
/** |
|
352
|
|
|
* Apply a patch on code in the specified directory. |
|
353
|
|
|
* |
|
354
|
|
|
* @param RemoteFilesystem $downloader |
|
355
|
|
|
* @param $install_path |
|
356
|
|
|
* @param $patch_url |
|
357
|
|
|
* @param PackageInterface $package |
|
358
|
|
|
* @throws \Exception |
|
359
|
|
|
*/ |
|
360
|
|
|
protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url, PackageInterface $package) { |
|
361
|
|
|
|
|
362
|
|
|
// Local patch file. |
|
363
|
|
|
if (file_exists($patch_url)) { |
|
364
|
|
|
$filename = realpath($patch_url); |
|
365
|
|
|
} |
|
366
|
|
|
else { |
|
367
|
|
|
// Generate random (but not cryptographically so) filename. |
|
368
|
|
|
$filename = uniqid(sys_get_temp_dir().'/') . ".patch"; |
|
369
|
|
|
|
|
370
|
|
|
// Download file from remote filesystem to this location. |
|
371
|
|
|
$hostname = parse_url($patch_url, PHP_URL_HOST); |
|
372
|
|
|
|
|
373
|
|
|
try { |
|
374
|
|
|
$downloader->copy($hostname, $patch_url, $filename, false); |
|
375
|
|
|
} catch (\Exception $e) { |
|
376
|
|
|
// In case of an exception, retry once as the download might |
|
377
|
|
|
// have failed due to intermittent network issues. |
|
378
|
|
|
$downloader->copy($hostname, $patch_url, $filename, false); |
|
379
|
|
|
} |
|
380
|
|
|
} |
|
381
|
|
|
|
|
382
|
|
|
// The order here is intentional. p1 is most likely to apply with git apply. |
|
383
|
|
|
// p0 is next likely. p2 is extremely unlikely, but for some special cases, |
|
384
|
|
|
// it might be useful. p4 is useful for Magento 2 patches |
|
385
|
|
|
$patch_levels = array('-p1', '-p0', '-p2', '-p4'); |
|
386
|
|
|
|
|
387
|
|
|
// Check for specified patch level for this package. |
|
388
|
|
|
$extra = $this->composer->getPackage()->getExtra(); |
|
389
|
|
|
if (!empty($extra['patchLevel'][$package->getName()])){ |
|
390
|
|
|
$patch_levels = array($extra['patchLevel'][$package->getName()]); |
|
391
|
|
|
} |
|
392
|
|
|
// Attempt to apply with git apply. |
|
393
|
|
|
$patched = $this->applyPatchWithGit($install_path, $patch_levels, $filename); |
|
394
|
|
|
|
|
395
|
|
|
// In some rare cases, git will fail to apply a patch, fallback to using |
|
396
|
|
|
// the 'patch' command. |
|
397
|
|
|
if (!$patched) { |
|
398
|
|
|
foreach ($patch_levels as $patch_level) { |
|
399
|
|
|
// --no-backup-if-mismatch here is a hack that fixes some |
|
400
|
|
|
// differences between how patch works on windows and unix. |
|
401
|
|
|
if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) { |
|
402
|
|
|
break; |
|
403
|
|
|
} |
|
404
|
|
|
} |
|
405
|
|
|
} |
|
406
|
|
|
|
|
407
|
|
|
// Clean up the temporary patch file. |
|
408
|
|
|
if (isset($hostname)) { |
|
409
|
|
|
unlink($filename); |
|
410
|
|
|
} |
|
411
|
|
|
// If the patch *still* isn't applied, then give up and throw an Exception. |
|
412
|
|
|
// Otherwise, let the user know it worked. |
|
413
|
|
|
if (!$patched) { |
|
414
|
|
|
throw new \Exception("Cannot apply patch $patch_url"); |
|
415
|
|
|
} |
|
416
|
|
|
} |
|
417
|
|
|
|
|
418
|
|
|
/** |
|
419
|
|
|
* Checks if the root package enables patching. |
|
420
|
|
|
* |
|
421
|
|
|
* @return bool |
|
422
|
|
|
* Whether patching is enabled. Defaults to TRUE. |
|
423
|
|
|
*/ |
|
424
|
|
|
protected function isPatchingEnabled() { |
|
425
|
|
|
$extra = $this->composer->getPackage()->getExtra(); |
|
426
|
|
|
|
|
427
|
|
|
if (empty($extra['patches']) && empty($extra['patches-ignore']) && !isset($extra['patches-file'])) { |
|
428
|
|
|
// The root package has no patches of its own, so only allow patching if |
|
429
|
|
|
// it has specifically opted in. |
|
430
|
|
|
return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE; |
|
431
|
|
|
} |
|
432
|
|
|
else { |
|
433
|
|
|
return TRUE; |
|
434
|
|
|
} |
|
435
|
|
|
} |
|
436
|
|
|
|
|
437
|
|
|
/** |
|
438
|
|
|
* Writes a patch report to the target directory. |
|
439
|
|
|
* |
|
440
|
|
|
* @param array $patches |
|
441
|
|
|
* @param string $directory |
|
442
|
|
|
*/ |
|
443
|
|
|
protected function writePatchReport($patches, $directory) { |
|
444
|
|
|
$output = "This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n"; |
|
445
|
|
|
$output .= "Patches applied to this directory:\n\n"; |
|
446
|
|
|
foreach ($patches as $description => $url) { |
|
447
|
|
|
$output .= $description . "\n"; |
|
448
|
|
|
$output .= 'Source: ' . $url . "\n\n\n"; |
|
449
|
|
|
} |
|
450
|
|
|
file_put_contents($directory . "/PATCHES.txt", $output); |
|
451
|
|
|
} |
|
452
|
|
|
|
|
453
|
|
|
/** |
|
454
|
|
|
* Executes a shell command with escaping. |
|
455
|
|
|
* |
|
456
|
|
|
* @param string $cmd |
|
457
|
|
|
* @return bool |
|
458
|
|
|
*/ |
|
459
|
|
|
protected function executeCommand($cmd) { |
|
460
|
|
|
// Shell-escape all arguments except the command. |
|
461
|
|
|
$args = func_get_args(); |
|
462
|
|
|
foreach ($args as $index => $arg) { |
|
463
|
|
|
if ($index !== 0) { |
|
464
|
|
|
$args[$index] = escapeshellarg($arg); |
|
465
|
|
|
} |
|
466
|
|
|
} |
|
467
|
|
|
|
|
468
|
|
|
// And replace the arguments. |
|
469
|
|
|
$command = call_user_func_array('sprintf', $args); |
|
470
|
|
|
$output = ''; |
|
471
|
|
|
if ($this->io->isVerbose()) { |
|
472
|
|
|
$this->io->write('<comment>' . $command . '</comment>'); |
|
473
|
|
|
$io = $this->io; |
|
474
|
|
|
$output = function ($type, $data) use ($io) { |
|
475
|
|
|
if ($type == Process::ERR) { |
|
476
|
|
|
$io->write('<error>' . $data . '</error>'); |
|
477
|
|
|
} |
|
478
|
|
|
else { |
|
479
|
|
|
$io->write('<comment>' . $data . '</comment>'); |
|
480
|
|
|
} |
|
481
|
|
|
}; |
|
482
|
|
|
} |
|
483
|
|
|
return ($this->executor->execute($command, $output) == 0); |
|
484
|
|
|
} |
|
485
|
|
|
|
|
486
|
|
|
/** |
|
487
|
|
|
* Recursively merge arrays without changing data types of values. |
|
488
|
|
|
* |
|
489
|
|
|
* Does not change the data types of the values in the arrays. Matching keys' |
|
490
|
|
|
* values in the second array overwrite those in the first array, as is the |
|
491
|
|
|
* case with array_merge. |
|
492
|
|
|
* |
|
493
|
|
|
* @param array $array1 |
|
494
|
|
|
* The first array. |
|
495
|
|
|
* @param array $array2 |
|
496
|
|
|
* The second array. |
|
497
|
|
|
* @return array |
|
498
|
|
|
* The merged array. |
|
499
|
|
|
* |
|
500
|
|
|
* @see http://php.net/manual/en/function.array-merge-recursive.php#92195 |
|
501
|
|
|
*/ |
|
502
|
|
|
protected function arrayMergeRecursiveDistinct(array $array1, array $array2) { |
|
503
|
|
|
$merged = $array1; |
|
504
|
|
|
|
|
505
|
|
|
foreach ($array2 as $key => &$value) { |
|
506
|
|
|
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { |
|
507
|
|
|
$merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value); |
|
508
|
|
|
} |
|
509
|
|
|
else { |
|
510
|
|
|
$merged[$key] = $value; |
|
511
|
|
|
} |
|
512
|
|
|
} |
|
513
|
|
|
|
|
514
|
|
|
return $merged; |
|
515
|
|
|
} |
|
516
|
|
|
|
|
517
|
|
|
/** |
|
518
|
|
|
* Attempts to apply a patch with git apply. |
|
519
|
|
|
* |
|
520
|
|
|
* @param $install_path |
|
521
|
|
|
* @param $patch_levels |
|
522
|
|
|
* @param $filename |
|
523
|
|
|
* |
|
524
|
|
|
* @return bool |
|
525
|
|
|
* TRUE if patch was applied, FALSE otherwise. |
|
526
|
|
|
*/ |
|
527
|
|
|
protected function applyPatchWithGit($install_path, $patch_levels, $filename) { |
|
528
|
|
|
// Do not use git apply unless the install path is itself a git repo |
|
529
|
|
|
// @see https://stackoverflow.com/a/27283285 |
|
530
|
|
|
if (!is_dir($install_path . '/.git')) { |
|
531
|
|
|
return FALSE; |
|
532
|
|
|
} |
|
533
|
|
|
|
|
534
|
|
|
$patched = FALSE; |
|
535
|
|
|
foreach ($patch_levels as $patch_level) { |
|
536
|
|
|
if ($this->io->isVerbose()) { |
|
537
|
|
|
$comment = 'Testing ability to patch with git apply.'; |
|
538
|
|
|
$comment .= ' This command may produce errors that can be safely ignored.'; |
|
539
|
|
|
$this->io->write('<comment>' . $comment . '</comment>'); |
|
540
|
|
|
} |
|
541
|
|
|
$checked = $this->executeCommand('git -C %s apply --check -v %s %s', $install_path, $patch_level, $filename); |
|
542
|
|
|
$output = $this->executor->getErrorOutput(); |
|
543
|
|
|
if (substr($output, 0, 7) == 'Skipped') { |
|
544
|
|
|
// Git will indicate success but silently skip patches in some scenarios. |
|
545
|
|
|
// |
|
546
|
|
|
// @see https://github.com/cweagans/composer-patches/pull/165 |
|
547
|
|
|
$checked = FALSE; |
|
548
|
|
|
} |
|
549
|
|
|
if ($checked) { |
|
550
|
|
|
// Apply the first successful style. |
|
551
|
|
|
$patched = $this->executeCommand('git -C %s apply %s %s', $install_path, $patch_level, $filename); |
|
552
|
|
|
break; |
|
553
|
|
|
} |
|
554
|
|
|
} |
|
555
|
|
|
return $patched; |
|
556
|
|
|
} |
|
557
|
|
|
|
|
558
|
|
|
} |
|
559
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.