ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
Patches.php
Go to the documentation of this file.
1<?php
2
9
10use Composer\Composer;
11use Composer\DependencyResolver\Operation\InstallOperation;
12use Composer\DependencyResolver\Operation\UninstallOperation;
13use Composer\DependencyResolver\Operation\UpdateOperation;
14use Composer\DependencyResolver\Operation\OperationInterface;
15use Composer\EventDispatcher\EventSubscriberInterface;
16use Composer\IO\IOInterface;
17use Composer\Package\AliasPackage;
18use Composer\Package\PackageInterface;
19use Composer\Plugin\PluginInterface;
20use Composer\Installer\PackageEvents;
21use Composer\Script\Event;
22use Composer\Script\ScriptEvents;
23use Composer\Script\PackageEvent;
24use Composer\Util\ProcessExecutor;
25use Composer\Util\RemoteFilesystem;
26use Symfony\Component\Process\Process;
27
28class Patches implements PluginInterface, EventSubscriberInterface {
29
33 protected $composer;
37 protected $io;
45 protected $executor;
49 protected $patches;
50
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 }
64
68 public static function getSubscribedEvents() {
69 return array(
70 ScriptEvents::PRE_INSTALL_CMD => "checkPatches",
71 ScriptEvents::PRE_UPDATE_CMD => "checkPatches",
72 PackageEvents::PRE_PACKAGE_INSTALL => "gatherPatches",
73 PackageEvents::PRE_PACKAGE_UPDATE => "gatherPatches",
74 PackageEvents::POST_PACKAGE_INSTALL => "postInstall",
75 PackageEvents::POST_PACKAGE_UPDATE => "postInstall",
76 );
77 }
78
83 public function checkPatches(Event $event) {
84 if (!$this->isPatchingEnabled()) {
85 return;
86 }
87
88 try {
89 $repositoryManager = $this->composer->getRepositoryManager();
90 $localRepository = $repositoryManager->getLocalRepository();
91 $installationManager = $this->composer->getInstallationManager();
92 $packages = $localRepository->getPackages();
93
94 $tmp_patches = $this->grabPatches();
95 if ($tmp_patches == FALSE) {
96 $this->io->write('<info>No patches supplied.</info>');
97 return;
98 }
99
100 foreach ($packages as $package) {
101 $extra = $package->getExtra();
102 $patches = isset($extra['patches']) ? $extra['patches'] : array();
103 $tmp_patches = array_merge_recursive($tmp_patches, $patches);
104 }
105
106 // Remove packages for which the patch set has changed.
107 foreach ($packages as $package) {
108 if (!($package instanceof AliasPackage)) {
109 $package_name = $package->getName();
110 $extra = $package->getExtra();
111 $has_patches = isset($tmp_patches[$package_name]);
112 $has_applied_patches = isset($extra['patches_applied']);
113 if (($has_patches && !$has_applied_patches)
114 || (!$has_patches && $has_applied_patches)
115 || ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra['patches_applied'])) {
116 $uninstallOperation = new UninstallOperation($package, 'Removing package so it can be re-installed and re-patched.');
117 $this->io->write('<info>Removing package ' . $package_name . ' so that it can be re-installed and re-patched.</info>');
118 $installationManager->uninstall($localRepository, $uninstallOperation);
119 }
120 }
121 }
122 }
123 // If the Locker isn't available, then we don't need to do this.
124 // It's the first time packages have been installed.
125 catch (\LogicException $e) {
126 return;
127 }
128 }
129
135 public function gatherPatches(PackageEvent $event) {
136 // If we've already done this, then don't do it again.
137 if (isset($this->patches['_patchesGathered'])) {
138 return;
139 }
140 // If patching has been disabled, bail out here.
141 elseif (!$this->isPatchingEnabled()) {
142 return;
143 }
144
145 $this->patches = $this->grabPatches();
146 if (empty($this->patches)) {
147 $this->io->write('<info>No patches supplied.</info>');
148 }
149
150 // Now add all the patches from dependencies that will be installed.
151 $operations = $event->getOperations();
152 $this->io->write('<info>Gathering patches for dependencies. This might take a minute.</info>');
153 foreach ($operations as $operation) {
154 if ($operation->getJobType() == 'install' || $operation->getJobType() == 'update') {
155 $package = $this->getPackageFromOperation($operation);
156 $extra = $package->getExtra();
157 if (isset($extra['patches'])) {
158 $this->patches = array_merge_recursive($this->patches, $extra['patches']);
159 }
160 }
161 }
162
163 // If we're in verbose mode, list the projects we're going to patch.
164 if ($this->io->isVerbose()) {
165 foreach ($this->patches as $package => $patches) {
166 $number = count($patches);
167 $this->io->write('<info>Found ' . $number . ' patches for ' . $package . '.</info>');
168 }
169 }
170
171 // Make sure we don't gather patches again. Extra keys in $this->patches
172 // won't hurt anything, so we'll just stash it there.
173 $this->patches['_patchesGathered'] = TRUE;
174 }
175
181 public function grabPatches() {
182 // First, try to get the patches from the root composer.json.
183 $extra = $this->composer->getPackage()->getExtra();
184 if (isset($extra['patches'])) {
185 $this->io->write('<info>Gathering patches for root package.</info>');
186 $patches = $extra['patches'];
187 return $patches;
188 }
189 // If it's not specified there, look for a patches-file definition.
190 elseif (isset($extra['patches-file'])) {
191 $this->io->write('<info>Gathering patches from patch file.</info>');
192 $patches = file_get_contents($extra['patches-file']);
193 $patches = json_decode($patches, TRUE);
194 $error = json_last_error();
195 if ($error != 0) {
196 switch ($error) {
197 case JSON_ERROR_DEPTH:
198 $msg = ' - Maximum stack depth exceeded';
199 break;
200 case JSON_ERROR_STATE_MISMATCH:
201 $msg = ' - Underflow or the modes mismatch';
202 break;
203 case JSON_ERROR_CTRL_CHAR:
204 $msg = ' - Unexpected control character found';
205 break;
206 case JSON_ERROR_SYNTAX:
207 $msg = ' - Syntax error, malformed JSON';
208 break;
209 case JSON_ERROR_UTF8:
210 $msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded';
211 break;
212 default:
213 $msg = ' - Unknown error';
214 break;
215 }
216 throw new \Exception('There was an error in the supplied patches file:' . $msg);
217 }
218 if (isset($patches['patches'])) {
219 $patches = $patches['patches'];
220 return $patches;
221 }
222 elseif(!$patches) {
223 throw new \Exception('There was an error in the supplied patch file');
224 }
225 }
226 else {
227 return array();
228 }
229 }
230
235 public function postInstall(PackageEvent $event) {
236 // Get the package object for the current operation.
237 $operation = $event->getOperation();
239 $package = $this->getPackageFromOperation($operation);
240 $package_name = $package->getName();
241
242 if (!isset($this->patches[$package_name])) {
243 if ($this->io->isVerbose()) {
244 $this->io->write('<info>No patches found for ' . $package_name . '.</info>');
245 }
246 return;
247 }
248 $this->io->write(' - Applying patches for <info>' . $package_name . '</info>');
249
250 // Get the install path from the package object.
251 $manager = $event->getComposer()->getInstallationManager();
252 $install_path = $manager->getInstaller($package->getType())->getInstallPath($package);
253
254 // Set up a downloader.
255 $downloader = new RemoteFilesystem($this->io, $this->composer->getConfig());
256
257 // Track applied patches in the package info in installed.json
258 $localRepository = $this->composer->getRepositoryManager()->getLocalRepository();
259 $localPackage = $localRepository->findPackage($package_name, $package->getVersion());
260 $extra = $localPackage->getExtra();
261 $extra['patches_applied'] = array();
262
263 foreach ($this->patches[$package_name] as $description => $url) {
264 $this->io->write(' <info>' . $url . '</info> (<comment>' . $description. '</comment>)');
265 try {
266 $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description));
267 $this->getAndApplyPatch($downloader, $install_path, $url);
268 $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description));
269 $extra['patches_applied'][$description] = $url;
270 }
271 catch (\Exception $e) {
272 $this->io->write(' <error>Could not apply patch! Skipping.</error>');
273 if (getenv('COMPOSER_EXIT_ON_PATCH_FAILURE')) {
274 throw new \Exception("Cannot apply patch $description ($url)!");
275 }
276 }
277 }
278 $localPackage->setExtra($extra);
279
280 $this->io->write('');
281 $this->writePatchReport($this->patches[$package_name], $install_path);
282 }
283
291 protected function getPackageFromOperation(OperationInterface $operation) {
292 if ($operation instanceof InstallOperation) {
293 $package = $operation->getPackage();
294 }
295 elseif ($operation instanceof UpdateOperation) {
296 $package = $operation->getTargetPackage();
297 }
298 else {
299 throw new \Exception('Unknown operation: ' . get_class($operation));
300 }
301
302 return $package;
303 }
304
313 protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url) {
314
315 // Local patch file.
316 if (file_exists($patch_url)) {
317 $filename = $patch_url;
318 }
319 else {
320 // Generate random (but not cryptographically so) filename.
321 $filename = uniqid("/tmp/") . ".patch";
322
323 // Download file from remote filesystem to this location.
324 $hostname = parse_url($patch_url, PHP_URL_HOST);
325 $downloader->copy($hostname, $patch_url, $filename, FALSE);
326 }
327
328 // Modified from drush6:make.project.inc
329 $patched = FALSE;
330 // The order here is intentional. p1 is most likely to apply with git apply.
331 // p0 is next likely. p2 is extremely unlikely, but for some special cases,
332 // it might be useful.
333 $patch_levels = array('-p1', '-p0', '-p2');
334 foreach ($patch_levels as $patch_level) {
335 $checked = $this->executeCommand('cd %s && GIT_DIR=. git apply --check %s %s', $install_path, $patch_level, $filename);
336 if ($checked) {
337 // Apply the first successful style.
338 $patched = $this->executeCommand('cd %s && GIT_DIR=. git apply %s %s', $install_path, $patch_level, $filename);
339 break;
340 }
341 }
342
343 // In some rare cases, git will fail to apply a patch, fallback to using
344 // the 'patch' command.
345 if (!$patched) {
346 foreach ($patch_levels as $patch_level) {
347 // --no-backup-if-mismatch here is a hack that fixes some
348 // differences between how patch works on windows and unix.
349 if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) {
350 break;
351 }
352 }
353 }
354
355 // Clean up the temporary patch file.
356 if (isset($hostname)) {
357 unlink($filename);
358 }
359 // If the patch *still* isn't applied, then give up and throw an Exception.
360 // Otherwise, let the user know it worked.
361 if (!$patched) {
362 throw new \Exception("Cannot apply patch $patch_url");
363 }
364 }
365
372 protected function isPatchingEnabled() {
373 $extra = $this->composer->getPackage()->getExtra();
374
375 if (empty($extra['patches'])) {
376 // The root package has no patches of its own, so only allow patching if
377 // it has specifically opted in.
378 return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE;
379 }
380 else {
381 return TRUE;
382 }
383 }
384
391 protected function writePatchReport($patches, $directory) {
392 $output = "This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n";
393 $output .= "Patches applied to this directory:\n\n";
394 foreach ($patches as $description => $url) {
395 $output .= $description . "\n";
396 $output .= 'Source: ' . $url . "\n\n\n";
397 }
398 file_put_contents($directory . "/PATCHES.txt", $output);
399 }
400
407 protected function executeCommand($cmd) {
408 // Shell-escape all arguments except the command.
409 $args = func_get_args();
410 foreach ($args as $index => $arg) {
411 if ($index !== 0) {
412 $args[$index] = escapeshellarg($arg);
413 }
414 }
415
416 // And replace the arguments.
417 $command = call_user_func_array('sprintf', $args);
418 $output = '';
419 if ($this->io->isVerbose()) {
420 $this->io->write('<comment>' . $command . '</comment>');
421 $io = $this->io;
422 $output = function ($type, $data) use ($io) {
423 if ($type == Process::ERR) {
424 $io->write('<error>' . $data . '</error>');
425 }
426 else {
427 $io->write('<comment>' . $data . '</comment>');
428 }
429 };
430 }
431 return ($this->executor->execute($command, $output) == 0);
432 }
433}
An exception for terminatinating execution or to throw for unit testing.
gatherPatches(PackageEvent $event)
Gather patches from dependencies and store them for later use.
Definition: Patches.php:135
getPackageFromOperation(OperationInterface $operation)
Get a Package object from an OperationInterface object.
Definition: Patches.php:291
activate(Composer $composer, IOInterface $io)
Apply plugin modifications to composer.
Definition: Patches.php:57
writePatchReport($patches, $directory)
Writes a patch report to the target directory.
Definition: Patches.php:391
executeCommand($cmd)
Executes a shell command with escaping.
Definition: Patches.php:407
static getSubscribedEvents()
Returns an array of event names this subscriber wants to listen to.
Definition: Patches.php:68
grabPatches()
Get the patches from root composer or external file.
Definition: Patches.php:181
checkPatches(Event $event)
Before running composer install,.
Definition: Patches.php:83
getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url)
Apply a patch on code in the specified directory.
Definition: Patches.php:313
isPatchingEnabled()
Checks if the root package enables patching.
Definition: Patches.php:372
if(!is_dir( $entity_dir)) exit("Fatal Error ([A-Za-z0-9]+)\s+" &#(? foreach( $entity_files as $file) $output
$error
Definition: Error.php:17
$index
Definition: metadata.php:60
$type
$url